aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-02-17 16:53:34 -0800
committerTor Norbye <tnorbye@google.com>2011-02-28 17:19:31 -0800
commit0757ce4af2764e4dd564acc0b1a013e910abc8da (patch)
tree630e43803cdc7d63aa05dafec73fd164f2616958 /eclipse
parent2695266bdddb8d827ea682a7e310e19919a73826 (diff)
downloadsdk-0757ce4af2764e4dd564acc0b1a013e910abc8da.zip
sdk-0757ce4af2764e4dd564acc0b1a013e910abc8da.tar.gz
sdk-0757ce4af2764e4dd564acc0b1a013e910abc8da.tar.bz2
More refactoring work: Convert hierarchy, and change type
A lot of work on the "Change Layout" refactoring to improve conversion to a Relative Layout. First, add a "Flatten Hierarchy" option which can take an entire hierarchy of layout widgets and flatten it down to a single top level RelativeLayout where the constraints attempt to reflect the original layout. (This isn't always possible, since some layout managers offer features not possible to express in RelativeLayout, such as a LinearLayout with multiple different weights) but it often works or is at least a good start. (This work is ongoing, but since my changeset is getting large I want to check in this snapshot since the functionality is better than what is in the trunk.) This changeset also adds a new refactoring: Change Widget Type. This can be applied to a selection of elements, and it will convert the widget type to the new target widget type. It will also remove any attributes that are not valid for the new layout. It also improves the wizards which display the possible target types. For Change Widget Type, it will first offer "related" widgets, so for an AnalogClock it will first offer Digital Clock, for a checkbox it will offer a checked text view and a radio button, etc. In addition, it will list Views and Layouts that it finds in any library jars (except for the builtin Android ones), and any custom view classes in the project. There is also now some preliminary support for refactoring unit tests. These tests must be run as Eclipse plugin tests, since they utilize the XML model (and the XML model cannot be mocked). The test infrastructure reads source XML files, applies the refactoring change list to them, and diffs the output with the known expected output (also stored as result XML files in the test project). Finally, there are a number of fixes and improvements to the shared refactoring code. Change-Id: I0974653e530dfb4feb625e0eef8257c29d50614b
Diffstat (limited to 'eclipse')
-rw-r--r--eclipse/dictionary.txt2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/.settings/org.moreunit.prefs4
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml19
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java31
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java37
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java9
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java51
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java60
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml316
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java134
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java70
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java254
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java200
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java9
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java1690
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java466
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java23
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java183
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java24
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java405
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java84
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoringTest.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoringTest.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringTest.java290
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoringTest.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout1-expected-extract1.xml2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout2-expected-extract2.xml6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeLayout1a.xml11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeView1.xml21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.info13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.xml21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b-expected-changeLayout1b.xml45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.info14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.xml70
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeLayout2.xml11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.info9
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.xml13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-changeLayout3.xml7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract1.xml7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract2.xml6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn1.xml11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn2.xml12
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn3.xml11
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.info5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.xml7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4-expected-changeLayout4.xml6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.info5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.xml8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-changeLayout5.xml10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.info9
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.xml12
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6-expected-changeLayout6.xml7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.info5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.xml7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java8
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java28
73 files changed, 4932 insertions, 228 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt
index 8246992..5e364be 100644
--- a/eclipse/dictionary.txt
+++ b/eclipse/dictionary.txt
@@ -16,6 +16,7 @@ apk
app
apps
arg
+async
attrs
avd
avds
@@ -122,6 +123,7 @@ marquee
metadata
min
monte
+ms
multi
multimap
multimaps
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.settings/org.moreunit.prefs b/eclipse/plugins/com.android.ide.eclipse.adt/.settings/org.moreunit.prefs
index ecb8e78..a1c97ef 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/.settings/org.moreunit.prefs
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/.settings/org.moreunit.prefs
@@ -1,4 +1,4 @@
-#Wed Nov 24 07:39:01 PST 2010
+#Mon Feb 28 13:14:31 PST 2011
eclipse.preferences.version=1
-org.moreunit.unitsourcefolder=adt\:src\:adt-tests\:unittests
+org.moreunit.unitsourcefolder=adt\:src\:adt-tests\:unittests\#adt\:src\:adt-tests\:src
org.moreunit.useprojectsettings=true
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index fe15099..62db218 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -679,6 +679,15 @@
style="push"
tooltip="Changes layouts from one type to another">
</action>
+ <action
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction"
+ definitionId="com.android.ide.eclipse.adt.refactoring.changeview"
+ id="com.android.ide.eclipse.adt.actions.ChangeView"
+ label="Change Widget Type..."
+ menubarPath="org.eclipse.jdt.ui.refactoring.menu/com.android.ide.eclipse.adt.refactoring.menu/android"
+ style="push"
+ tooltip="Changes the type of the selected widgets">
+ </action>
<menu
id="org.eclipse.jdt.ui.refactoring.menu"
label="Refactor">
@@ -806,6 +815,12 @@
id="com.android.ide.eclipse.adt.refactoring.convert"
name="Change Layout">
</command>
+ <command
+ categoryId="com.android.ide.eclipse.adt.refactoring.category"
+ description="Changes the widget type for the selection"
+ id="com.android.ide.eclipse.adt.refactoring.changeview"
+ name="Change Widget Type">
+ </command>
</extension>
<extension
point="org.eclipse.ltk.core.refactoring.refactoringContributions">
@@ -825,6 +840,10 @@
class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutContribution"
id="com.android.ide.eclipse.adt.refactoring.convert">
</contribution>
+ <contribution
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewContribution"
+ id="com.android.ide.eclipse.adt.refactoring.changeview">
+ </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/AbsoluteLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java
index 4af4559..c8ce3ac 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/AbsoluteLayoutRule.java
@@ -17,6 +17,7 @@
package com.android.ide.common.layout;
import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DIP;
import com.android.ide.common.api.DrawingStyle;
import com.android.ide.common.api.DropFeedback;
@@ -189,9 +190,9 @@ public class AbsoluteLayoutRule extends BaseLayoutRule {
}
newChild.setAttribute(ANDROID_URI, "layout_x", //$NON-NLS-1$
- x + "dip"); //$NON-NLS-1$
+ String.format(VALUE_N_DIP, x));
newChild.setAttribute(ANDROID_URI, "layout_y", //$NON-NLS-1$
- y + "dip"); //$NON-NLS-1$
+ String.format(VALUE_N_DIP, y));
addInnerElements(newChild, element, idMap);
}
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 2ba2104..97da864c 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
@@ -44,13 +44,17 @@ public class LayoutConstants {
public static final String GALLERY = "Gallery"; //$NON-NLS-1$
public static final String GRID_VIEW = "GridView"; //$NON-NLS-1$
public static final String SCROLL_VIEW = "ScrollView"; //$NON-NLS-1$
+ public static final String RADIO_BUTTON = "RadioButton"; //$NON-NLS-1$
+ public static final String RADIO_GROUP = "RadioGroup"; //$NON-NLS-1$
public static final String EXPANDABLE_LIST_VIEW = "ExpandableListView";//$NON-NLS-1$
+ public static final String GESTURE_OVERLAY_VIEW = "GestureOverlayView";//$NON-NLS-1$
public static final String ATTR_TEXT = "text"; //$NON-NLS-1$
public static final String ATTR_ID = "id"; //$NON-NLS-1$
public static final String ATTR_HANDLE = "handle"; //$NON-NLS-1$
public static final String ATTR_CONTENT = "content"; //$NON-NLS-1$
public static final String ATTR_CHECKED = "checked"; //$NON-NLS-1$
+ public static final String ATTR_BACKGROUND = "background"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_PREFIX = "layout_"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_HEIGHT = "layout_height"; //$NON-NLS-1$
@@ -64,10 +68,16 @@ public class LayoutConstants {
public static final String ATTR_LAYOUT_MARGIN_TOP = "layout_marginTop"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_MARGIN_BOTTOM = "layout_marginBottom"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_LEFT = "layout_alignLeft"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_RIGHT = "layout_alignRight"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_TOP = "layout_alignTop"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_BOTTOM = "layout_alignBottom"; //$NON-NLS-1$
+
public static final String ATTR_LAYOUT_ALIGN_PARENT_TOP = "layout_alignParentTop"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_ALIGN_PARENT_BOTTOM = "layout_alignParentBottom"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_ALIGN_PARENT_LEFT = "layout_alignParentLeft";//$NON-NLS-1$
public static final String ATTR_LAYOUT_ALIGN_PARENT_RIGHT = "layout_alignParentRight"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING = "layout_alignWithParentMissing"; //$NON-NLS-1$
public static final String ATTR_LAYOUT_ALIGN_BASELINE = "layout_alignBaseline"; //$NON-NLS-1$
@@ -108,6 +118,21 @@ public class LayoutConstants {
public static final String VALUE_ALIGN_WITH_PARENT_MISSING =
"alignWithParentMissing"; //$NON-NLS-1$
+ // Gravity values. These have the GRAVITY_ prefix in front of value because we already
+ // have VALUE_CENTER_HORIZONTAL defined for layouts, and its definition conflicts
+ // (centerHorizontal versus center_horizontal)
+ public static final String GRAVITY_VALUE_ = "center"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_CENTER = "center"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_RIGHT = "right"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_LEFT = "left"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_BOTTOM = "bottom"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_TOP = "top"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_FILL_HORIZONTAL = "fill_horizontal"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_FILL_VERTICAL = "fill_vertical"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_CENTER_HORIZONTAL = "center_horizontal"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_CENTER_VERTICAL = "center_vertical"; //$NON-NLS-1$
+ public static final String GRAVITY_VALUE_FILL = "fill"; //$NON-NLS-1$
+
/** The default prefix used for the {@link #ANDROID_URI} name space */
public static final String ANDROID_NS_PREFIX = "android"; //$NON-NLS-1$
@@ -153,6 +178,12 @@ public class LayoutConstants {
/** The fully qualified class name of an AdapterView */
public static final String FQCN_ADAPTER_VIEW = "android.widget.AdapterView"; //$NON-NLS-1$
+ /** The fully qualified class name of a GestureOverlayView */
+ public static final String FQCN_GESTURE_OVERLAY_VIEW = "android.gesture.GestureOverlayView"; //$NON-NLS-1$
+
+ /** The fully qualified class name of a RadioGroup */
+ public static final String FQCN_RADIO_GROUP = "android.widgets.RadioGroup"; //$NON-NLS-1$
+
public static final String ATTR_SRC = "src"; //$NON-NLS-1$
// like fill_parent for API 8
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 6d42fdb..c73cd6f 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
@@ -661,7 +661,7 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
* Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
* Editor) or null if not available.
*/
- public final IStructuredDocument getStructuredDocument() {
+ public IStructuredDocument getStructuredDocument() {
if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
}
@@ -681,7 +681,7 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
*
* @return The model for the XML document or null if cannot be obtained from the editor
*/
- public final IStructuredModel getModelForRead() {
+ public IStructuredModel getModelForRead() {
IStructuredDocument document = getStructuredDocument();
if (document != null) {
IModelManager mm = StructuredModelManager.getModelManager();
@@ -710,7 +710,6 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
* @return The model for the XML document or null if cannot be obtained from the editor
*/
private IStructuredModel getModelForEdit() {
-
IStructuredDocument document = getStructuredDocument();
if (document != null) {
IModelManager mm = StructuredModelManager.getModelManager();
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 aed740f..b862ada 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
@@ -41,6 +41,7 @@ import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
@@ -70,6 +71,9 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
/** Root node of the UI element hierarchy */
private UiDocumentNode mUiRootNode;
+ /** Flag used to ignore XML model updates */
+ private boolean mIgnoreXmlUpdate;
+
private GraphicalEditorPart mGraphicalEditor;
private int mGraphicalEditorIndex;
/** Implementation of the {@link IContentOutlinePage} for this editor */
@@ -286,12 +290,45 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
}
/**
+ * Controls whether XML models are ignored or not. Can be used in conjunction with the
+ * {@link #refreshXmlModel()} method to suppress many smaller individual edits and
+ * then flush a large model update at the end. This is for example used for
+ * refactoring, such that we don't update the model as each individual
+ * {@link TextEdit} is applied, and instead we process them all in a single refresh at
+ * the end.
+ *
+ * @param ignore when true, ignore all subsequent XML model updates, when false start
+ * processing XML model updates again
+ */
+ public void setIgnoreXmlUpdate(boolean ignore) {
+ mIgnoreXmlUpdate = ignore;
+ }
+
+ /** Performs a complete refresh of the XML model */
+ public void refreshXmlModel() {
+ Document xmlDoc = mUiRootNode.getXmlDocument();
+
+ initUiRootNode(true /*force*/);
+ mUiRootNode.loadFromXmlNode(xmlDoc);
+ // update the model first, since it is used by the viewers.
+ super.xmlModelChanged(xmlDoc);
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+ }
+
+ /**
* Processes the new XML Model, which XML root node is given.
*
* @param xml_doc The XML document, if available, or null if none exists.
*/
@Override
protected void xmlModelChanged(Document xml_doc) {
+ if (mIgnoreXmlUpdate) {
+ return;
+ }
+
// init the ui root on demand
initUiRootNode(false /*force*/);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
index 6c32445..30dc3e9 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+import static com.android.ide.common.layout.LayoutConstants.GESTURE_OVERLAY_VIEW;
import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
import com.android.ide.common.api.Rect;
@@ -352,7 +353,7 @@ public class CanvasViewInfo implements IPropertySource {
// root as well (such that the whole layout canvas does not highlight as part of hovers
// etc)
if (mParent != null
- && mParent.mName.endsWith("GestureOverlayView") //$NON-NLS-1$
+ && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW)
&& mParent.isRoot()) {
return true;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
index f056ea5..032fe28 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
@@ -17,6 +17,8 @@ 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 static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.util.Pair;
@@ -258,7 +260,13 @@ public class DomUtilities {
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());
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ seen.add(id.substring(NEW_ID_PREFIX.length()).toLowerCase());
+ } else if (id.startsWith(ID_PREFIX)) {
+ seen.add(id.substring(ID_PREFIX.length()).toLowerCase());
+ } else {
+ seen.add(id.toLowerCase());
+ }
}
}
@@ -267,19 +275,29 @@ public class DomUtilities {
* given element, which is guaranteed to be unique in this document
*
* @param element the element to compute a new widget id for
+ * @param reserved an optional set of extra, "reserved" set of ids that should be
+ * considered taken
+ * @param prefix an optional prefix to use for the generated name, or null to get a
+ * default (which is currently the tag name)
* @return a unique id, never null, which does not include the {@code @id/} prefix
* @see DescriptorsUtils#getFreeWidgetId
*/
- public static String getFreeWidgetId(Element element) {
+ public static String getFreeWidgetId(Element element, Set<String> reserved, String prefix) {
Set<String> ids = new HashSet<String>();
+ if (reserved != null) {
+ for (String id : reserved) {
+ ids.add(id.toLowerCase());
+ }
+ }
addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
- String prefix = element.getTagName();
+ if (prefix == null) {
+ prefix = element.getTagName();
+ }
String generated;
int num = 1;
do {
- num++;
- generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ 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 d1ab0ca..a0db1fb 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
@@ -16,6 +16,8 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_GESTURE_OVERLAY_VIEW;
+
import com.android.ide.common.api.IMenuCallback;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.MenuAction;
@@ -24,6 +26,7 @@ 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.ChangeViewAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
@@ -207,13 +210,22 @@ import java.util.regex.Pattern;
}
private void insertVisualRefactorings(String endId) {
- // Extract As <include> refactoring.
+ // Extract As <include> refactoring, Wrap In Refactoring, etc.
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
// 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
mMenuManager.insertBefore(endId, new Separator());
mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditor));
mMenuManager.insertBefore(endId, WrapInAction.create(mEditor));
- mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditor));
+ if (selection.size() == 1 && (selection.get(0).isLayout() ||
+ selection.get(0).getViewInfo().getName().equals(FQCN_GESTURE_OVERLAY_VIEW))) {
+ mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditor));
+ } else {
+ mMenuManager.insertBefore(endId, ChangeViewAction.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/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
index 84db9fb..1e9eebe 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
@@ -139,4 +139,13 @@ class SelectionItem {
return elements.toArray(new SimpleElement[elements.size()]);
}
+
+ /**
+ * Returns true if this selection item is a layout
+ *
+ * @return true if this selection item is a layout
+ */
+ public boolean isLayout() {
+ return mCanvasViewInfo.getUiViewNode().getDescriptor().hasChildren();
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
index 91475d5..16d8f43 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -44,6 +44,8 @@ import java.util.Set;
* operations on this set.
*/
public class ViewHierarchy {
+ private static final boolean DUMP_INFO = false;
+
private LayoutCanvas mCanvas;
/**
@@ -168,6 +170,9 @@ public class ViewHierarchy {
ViewInfo root = rootList.get(0);
if (root != null) {
infos = CanvasViewInfo.create(root);
+ if (DUMP_INFO) {
+ dump(root, 0);
+ }
} else {
infos = null;
}
@@ -195,7 +200,7 @@ public class ViewHierarchy {
mIncludedBounds = null;
}
- updateNodeProxies(mLastValidViewInfoRoot, null);
+ updateNodeProxies(mLastValidViewInfoRoot);
// Update the data structures related to tracking invisible and exploded nodes.
// We need to find the {@link CanvasViewInfo} objects that correspond to
@@ -252,7 +257,7 @@ public class ViewHierarchy {
* This is a recursive call that updates the whole hierarchy starting at the given
* view info.
*/
- private void updateNodeProxies(CanvasViewInfo vi, UiViewElementNode parentKey) {
+ private void updateNodeProxies(CanvasViewInfo vi) {
if (vi == null) {
return;
}
@@ -264,7 +269,7 @@ public class ViewHierarchy {
}
for (CanvasViewInfo child : vi.getChildren()) {
- updateNodeProxies(child, key);
+ updateNodeProxies(child);
}
}
@@ -684,4 +689,44 @@ public class ViewHierarchy {
return mIncludedBounds;
}
+ /**
+ * Dumps a {@link ViewInfo} hierarchy to stdout
+ *
+ * @param info the {@link ViewInfo} object to dump
+ * @param depth the depth to indent it to
+ */
+ public static void dump(ViewInfo info, int depth) {
+ if (DUMP_INFO) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append(" "); //$NON-NLS-1$
+ }
+ sb.append(info.getClassName());
+ sb.append(" ["); //$NON-NLS-1$
+ sb.append(info.getLeft());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getTop());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getRight());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getBottom());
+ sb.append("]"); //$NON-NLS-1$
+ Object cookie = info.getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ sb.append(" "); //$NON-NLS-1$
+ UiViewElementNode node = (UiViewElementNode) cookie;
+ sb.append("<"); //$NON-NLS-1$
+ sb.append(node.getDescriptor().getXmlName());
+ sb.append(">"); //$NON-NLS-1$
+ } else if (cookie != null) {
+ sb.append(" " + cookie); //$NON-NLS-1$
+ }
+
+ System.out.println(sb.toString());
+
+ for (ViewInfo child : info.getChildren()) {
+ dump(child, depth + 1);
+ }
+ }
+ }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
index 589ad2d..567f5d5 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java
@@ -22,6 +22,7 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
+import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.IViewMetadata.FillPreference;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
@@ -38,6 +39,7 @@ import org.xml.sax.InputSource;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -233,9 +235,10 @@ public class ViewMetadataRepository {
if (render.length() > 0) {
mode = RenderMode.get(render);
}
+ String relatedTo = child.getAttribute("relatedTo"); //$NON-NLS-1$
ViewData view = new ViewData(category, fqcn, fillPreference,
skip.length() == 0 ? false : Boolean.valueOf(skip),
- mode);
+ mode, relatedTo);
category.addView(view);
}
}
@@ -369,6 +372,11 @@ public class ViewMetadataRepository {
return result;
}
+ @VisibleForTesting
+ Collection<String> getAllFqcns() {
+ return getClassToView().keySet();
+ }
+
/**
* Metadata holder for a particular category - contains the name of the category, its
* ordinal (for natural/logical sorting order) and views contained in the category
@@ -424,18 +432,22 @@ public class ViewMetadataRepository {
private final boolean mSkip;
/** Must this item be rendered alone? skipped? etc */
private final RenderMode mRenderMode;
+ /** Related views */
+ private final String mRelatedTo;
/** The relative rank of the view for natural ordering */
private final int mOrdinal = sNextOrdinal++;
/** Constructs a new view data for the given class */
private ViewData(CategoryData category, String fqcn,
- FillPreference fillPreference, boolean skip, RenderMode renderMode) {
+ FillPreference fillPreference, boolean skip, RenderMode renderMode,
+ String relatedTo) {
super();
mCategory = category;
mFqcn = fqcn;
mFillPreference = fillPreference;
mSkip = skip;
mRenderMode = renderMode;
+ mRelatedTo = relatedTo;
}
/** Returns the category for views of this type */
@@ -470,6 +482,33 @@ public class ViewMetadataRepository {
public boolean getSkip() {
return mSkip;
}
+
+ public List<String> getRelatedTo() {
+ if (mRelatedTo == null || mRelatedTo.length() == 0) {
+ return Collections.emptyList();
+ } else {
+ String[] basenames = mRelatedTo.split(","); //$NON-NLS-1$
+ List<String> result = new ArrayList<String>();
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ Map<String, ViewData> classToView = repository.getClassToView();
+
+ List<String> fqns = new ArrayList<String>(classToView.keySet());
+ for (String basename : basenames) {
+ boolean found = false;
+ for (String fqcn : fqns) {
+ String suffix = '.' + basename;
+ if (fqcn.endsWith(suffix)) {
+ result.add(fqcn);
+ found = true;
+ break;
+ }
+ }
+ assert found : basename;
+ }
+
+ return result;
+ }
+ }
}
/**
@@ -520,6 +559,23 @@ public class ViewMetadataRepository {
return false;
}
+ /**
+ * Returns a set of fully qualified names for views that are closely related to the
+ * given view
+ *
+ * @param fqcn the fully qualified class name
+ * @return a list, never null but possibly empty, of views that are related to the
+ * view of the given type
+ */
+ public List<String> getRelatedTo(String fqcn) {
+ ViewData view = getClassToView().get(fqcn);
+ if (view != null) {
+ return view.getRelatedTo();
+ }
+
+ return Collections.emptyList();
+ }
+
/** Render mode for palette preview */
public enum RenderMode {
/**
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
index 627c2e9..262f63d 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Palette Metadata
+ <!--
+ Palette Metadata
- This document provides additional designtime metadata for various Android views,
- such as logical palette categories (as well as a natural ordering of the views within
- their categories, fill-preferences (how a view will sets its width and height attributes
- when dropped into other views), and so on.
- -->
+ This document provides additional designtime metadata for various Android views, such as
+ logical palette categories (as well as a natural ordering of the views within their
+ categories, fill-preferences (how a view will sets its width and height attributes when
+ dropped into other views), and so on.
+ -->
<!DOCTYPE metadata [
<!--- The metadata consists of a series of category definitions -->
<!ELEMENT metadata (category)*>
@@ -18,7 +18,7 @@
<!ELEMENT view EMPTY>
<!ATTLIST view
class CDATA #REQUIRED
- previewXml CDATA #IMPLIED
+ relatedTo CDATA #IMPLIED
skip (true|false) "false"
render (alone|skip|normal) "normal"
fill ( none|both|width|height|opposite|
@@ -26,82 +26,244 @@
>
]>
<metadata>
- <category name="Form Widgets">
- <view class="android.widget.TextView" />
- <view class="android.widget.Button" />
- <view class="android.widget.CheckBox" />
- <view class="android.widget.ToggleButton" />
- <view class="android.widget.RadioButton" />
- <view class="android.widget.CheckedTextView" />
- <view class="android.widget.Spinner" fill="width_in_vertical" />
- <view class="android.widget.EditText" fill="width_in_vertical" />
- <view class="android.widget.AutoCompleteTextView" fill="width_in_vertical" />
- <view class="android.widget.MultiAutoCompleteTextView" fill="width_in_vertical" />
- <view class="android.widget.ProgressBar" />
- <view class="android.widget.QuickContactBadge" />
- <view class="android.widget.RadioGroup" />
- <view class="android.widget.RatingBar" />
- <view class="android.widget.SeekBar" fill="width_in_vertical" />
+ <category
+ name="Form Widgets">
+ <view
+ class="android.widget.TextView"
+ relatedTo="EditText,AutoCompleteTextView,MultiAutoCompleteTextView" />
+ <view
+ class="android.widget.Button"
+ relatedTo="ImageButton" />
+ <view
+ class="android.widget.CheckBox"
+ relatedTo="RadioButton,ToggleButton,CheckedTextView" />
+ <view
+ class="android.widget.ToggleButton"
+ relatedTo="CheckBox" />
+ <view
+ class="android.widget.RadioButton"
+ relatedTo="CheckBox,ToggleButton" />
+ <view
+ class="android.widget.CheckedTextView"
+ relatedTo="TextView,CheckBox" />
+ <view
+ class="android.widget.Spinner"
+ relatedTo="EditText"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.EditText"
+ relatedTo="Spinner,TextView,AutoCompleteTextView,MultiAutoCompleteTextView"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.AutoCompleteTextView"
+ relatedTo="EditText,MultiAutoCompleteTextView"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.MultiAutoCompleteTextView"
+ relatedTo="EditText,AutoCompleteTextView"
+ fill="width_in_vertical" />
+ <view
+ class="android.widget.ProgressBar"
+ relatedTo="SeekBar" />
+ <view
+ class="android.widget.QuickContactBadge" />
+ <view
+ class="android.widget.RadioGroup" />
+ <view
+ class="android.widget.RatingBar" />
+ <view
+ class="android.widget.SeekBar"
+ relatedTo="ProgressBar"
+ fill="width_in_vertical" />
</category>
- <category name="Layouts">
- <view class="android.widget.LinearLayout" fill="opposite" render="skip"/>
- <view class="android.widget.RelativeLayout" fill="opposite" render="skip"/>
- <view class="android.widget.FrameLayout" fill="opposite" render="skip"/>
- <view class="android.widget.TableLayout" fill="opposite" render="skip"/>
- <view class="android.widget.TableRow" fill="opposite" render="skip"/>
+ <category
+ name="Layouts">
+ <view
+ class="android.widget.LinearLayout"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.RelativeLayout"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.FrameLayout"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.TableLayout"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.TableRow"
+ fill="opposite"
+ render="skip" />
</category>
- <category name="Composite">
- <view class="android.widget.ListView" fill="width_in_vertical" render="skip"/>
- <view class="android.widget.ExpandableListView" fill="width_in_vertical" render="skip"/>
- <view class="android.widget.TwoLineListItem" render="skip"/>
- <view class="android.widget.GridView" fill="opposite" render="skip"/>
- <view class="android.widget.ScrollView" fill="opposite" render="skip"/>
- <view class="android.widget.HorizontalScrollView" render="skip"/>
- <view class="android.widget.SearchView" render="skip"/>
- <view class="android.widget.SlidingDrawer"/>
- <view class="android.widget.TabHost" fill="width_in_vertical" render="alone" />
- <view class="android.widget.TabWidget" render="alone" />
- <view class="android.webkit.WebView" fill="opposite" render="skip" />
+ <category
+ name="Composite">
+ <view
+ class="android.widget.ListView"
+ relatedTo="ExpandableListView"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.ExpandableListView"
+ relatedTo="ListView"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.TwoLineListItem"
+ render="skip" />
+ <view
+ class="android.widget.GridView"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ScrollView"
+ relatedTo="HorizontalScrollView"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.HorizontalScrollView"
+ relatedTo="ScrollView"
+ render="skip" />
+ <view
+ class="android.widget.SearchView"
+ render="skip" />
+ <view
+ class="android.widget.SlidingDrawer" />
+ <view
+ class="android.widget.TabHost"
+ fill="width_in_vertical"
+ render="alone" />
+ <view
+ class="android.widget.TabWidget"
+ render="alone" />
+ <view
+ class="android.webkit.WebView"
+ fill="opposite"
+ render="skip" />
</category>
- <category name="Images &amp; Media">
- <view class="android.widget.ImageView" />
- <view class="android.widget.ImageButton" />
- <view class="android.widget.Gallery" fill="width_in_vertical" render="skip"/>
- <view class="android.widget.MediaController" render="skip"/>
- <view class="android.widget.VideoView" fill="opposite" render="skip"/>
+ <category
+ name="Images &amp; Media">
+ <view
+ class="android.widget.ImageView"
+ relatedTo="ImageButton,VideoView" />
+ <view
+ class="android.widget.ImageButton"
+ relatedTo="Button,ImageView" />
+ <view
+ class="android.widget.Gallery"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.MediaController"
+ render="skip" />
+ <view
+ class="android.widget.VideoView"
+ relatedTo="ImageView"
+ fill="opposite"
+ render="skip" />
</category>
- <category name="Time &amp; Date">
- <view class="android.widget.TimePicker" render="alone"/>
- <view class="android.widget.DatePicker" render="alone"/>
- <view class="android.widget.CalendarView" />
- <view class="android.widget.Chronometer" />
- <view class="android.widget.AnalogClock" />
- <view class="android.widget.DigitalClock" />
+ <category
+ name="Time &amp; Date">
+ <view
+ class="android.widget.TimePicker"
+ relatedTo="DatePicker,CalendarView"
+ render="alone" />
+ <view
+ class="android.widget.DatePicker"
+ relatedTo="TimePicker"
+ render="alone" />
+ <view
+ class="android.widget.CalendarView"
+ relatedTo="TimePicker,DatePicker" />
+ <view
+ class="android.widget.Chronometer" />
+ <view
+ class="android.widget.AnalogClock"
+ relatedTo="DigitalClock" />
+ <view
+ class="android.widget.DigitalClock"
+ relatedTo="AnalogClock" />
</category>
- <category name="Transitions">
- <view class="android.widget.ImageSwitcher" render="skip"/>
- <view class="android.widget.AdapterViewFlipper" fill="opposite" render="skip"/>
- <view class="android.widget.StackView" fill="opposite" render="skip"/>
- <view class="android.widget.TextSwitcher" fill="opposite" render="skip"/>
- <view class="android.widget.ViewAnimator" fill="opposite" render="skip"/>
- <view class="android.widget.ViewFlipper" fill="opposite" render="skip"/>
- <view class="android.widget.ViewSwitcher" fill="opposite" render="skip"/>
+ <category
+ name="Transitions">
+ <view
+ class="android.widget.ImageSwitcher"
+ relatedTo="ViewFlipper,ViewSwitcher,TextSwitcher"
+ render="skip" />
+ <view
+ class="android.widget.AdapterViewFlipper"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.StackView"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.TextSwitcher"
+ relatedTo="ViewFlipper,ImageSwitcher,ViewSwitcher"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewAnimator"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewFlipper"
+ relatedTo="ViewSwitcher,ImageSwitcher,TextSwitcher"
+ fill="opposite"
+ render="skip" />
+ <view
+ class="android.widget.ViewSwitcher"
+ relatedTo="ViewFlipper,ImageSwitcher,TextSwitcher"
+ fill="opposite"
+ render="skip" />
</category>
- <category name="Advanced">
- <view class="android.view.View" render="skip"/>
- <view class="android.view.ViewStub" render="skip"/>
- <view class="android.gesture.GestureOverlayView" render="skip"/>
- <view class="android.view.SurfaceView" render="skip"/>
- <view class="android.widget.NumberPicker" render="alone"/>
- <view class="android.widget.ZoomButton"/>
- <view class="android.widget.ZoomControls"/>
- <view class="include" skip="true" render="skip"/>
- <view class="merge" skip="true" render="skip"/>
- <view class="android.widget.DialerFilter" fill="width_in_vertical" render="skip"/>
- <view class="android.widget.AbsoluteLayout" fill="opposite" render="skip"/>
+ <category
+ name="Advanced">
+ <view
+ class="android.view.View"
+ render="skip" />
+ <view
+ class="android.view.ViewStub"
+ render="skip" />
+ <view
+ class="android.gesture.GestureOverlayView"
+ render="skip" />
+ <view
+ class="android.view.SurfaceView"
+ render="skip" />
+ <view
+ class="android.widget.NumberPicker"
+ relatedTo="TimePicker,DatePicker"
+ render="alone" />
+ <view
+ class="android.widget.ZoomButton"
+ relatedTo="Button" />
+ <view
+ class="android.widget.ZoomControls" />
+ <view
+ class="include"
+ skip="true"
+ render="skip" />
+ <view
+ class="merge"
+ skip="true"
+ render="skip" />
+ <view
+ class="android.widget.DialerFilter"
+ fill="width_in_vertical"
+ render="skip" />
+ <view
+ class="android.widget.AbsoluteLayout"
+ fill="opposite"
+ render="skip" />
</category>
- <category name="Other">
+ <category
+ name="Other">
<!-- This is the catch-all category which contains unknown views if we encounter any -->
</category>
<!-- TODO: Add-ons? -->
-</metadata>
+</metadata> \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java
index cb6eabf..7029be4 100644
--- 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
@@ -31,7 +31,7 @@ public class ChangeLayoutAction extends VisualRefactoringAction {
if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
ChangeLayoutRefactoring ref = new ChangeLayoutRefactoring(mFile, mEditor,
mTextSelection, mTreeSelection);
- RefactoringWizard wizard = new ChangeLayoutWizard(ref, mFile.getProject());
+ RefactoringWizard wizard = new ChangeLayoutWizard(ref, mEditor);
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
index a83d41f..b12cb62 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
@@ -22,21 +22,24 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BA
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_GESTURE_OVERLAY_VIEW;
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.GESTURE_OVERLAY_VIEW;
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.AdtConstants.EXT_XML;
+import com.android.annotations.VisibleForTesting;
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 com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
@@ -67,9 +70,11 @@ import java.util.Set;
*/
@SuppressWarnings("restriction") // XML model
public class ChangeLayoutRefactoring extends VisualRefactoring {
- private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+ private static final String KEY_FLATTEN = "flatten"; //$NON-NLS-1$
private String mTypeFqcn;
+ private boolean mFlatten;
/**
* This constructor is solely used by {@link Descriptor},
@@ -79,6 +84,12 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
ChangeLayoutRefactoring(Map<String, String> arguments) {
super(arguments);
mTypeFqcn = arguments.get(KEY_TYPE);
+ mFlatten = Boolean.parseBoolean(arguments.get(KEY_FLATTEN));
+ }
+
+ @VisibleForTesting
+ ChangeLayoutRefactoring(List<Element> selectedElements, LayoutEditor editor) {
+ super(selectedElements, editor);
}
public ChangeLayoutRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
@@ -99,7 +110,6 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
return status;
}
- mElements = getElements();
if (mElements.size() != 1) {
status.addFatalError("Select precisely one layout to convert");
return status;
@@ -127,6 +137,7 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
protected Map<String, String> createArgumentMap() {
Map<String, String> args = super.createArgumentMap();
args.put(KEY_TYPE, mTypeFqcn);
+ args.put(KEY_FLATTEN, Boolean.toString(mFlatten));
return args;
}
@@ -140,6 +151,48 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
mTypeFqcn = typeFqcn;
}
+ void setFlatten(boolean flatten) {
+ mFlatten = flatten;
+ }
+
+ @Override
+ protected List<Element> initElements() {
+ List<Element> elements = super.initElements();
+
+ // Don't convert a root GestureOverlayView; convert its child. This looks for
+ // gesture overlays, and if found, it generates a new child list where the gesture
+ // overlay children are replaced by their first element children
+ for (Element element : elements) {
+ String tagName = element.getTagName();
+ if (tagName.equals(GESTURE_OVERLAY_VIEW)
+ || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
+ List<Element> replacement = new ArrayList<Element>(elements.size());
+ for (Element e : elements) {
+ tagName = e.getTagName();
+ if (tagName.equals(GESTURE_OVERLAY_VIEW)
+ || tagName.equals(FQCN_GESTURE_OVERLAY_VIEW)) {
+ NodeList children = e.getChildNodes();
+ Element first = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ first = (Element) node;
+ break;
+ }
+ }
+ if (first != null) {
+ e = first;
+ }
+ }
+ replacement.add(e);
+ }
+ return replacement;
+ }
+ }
+
+ return elements;
+ }
+
@Override
protected List<Change> computeChanges() {
String name = getViewClass(mTypeFqcn);
@@ -166,21 +219,35 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
}
}
+ ensureIdMatchesType(layout, mTypeFqcn, rootEdit);
+
String oldType = getOldType();
String newType = mTypeFqcn;
- if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_RELATIVE_LAYOUT)) {
- // Hand-coded conversion
- convertLinearToRelative(rootEdit);
+ if (newType.equals(FQCN_RELATIVE_LAYOUT)) {
+ if (oldType.equals(FQCN_LINEAR_LAYOUT) && !mFlatten) {
+ // Hand-coded conversion specifically tailored for linear to relative, provided
+ // there is no hierarchy flattening
+ // TODO: use the RelativeLayoutConversionHelper for this; it does a better job
+ // analyzing gravities etc.
+ convertLinearToRelative(rootEdit);
+ removeUndefinedLayoutAttrs(rootEdit, layout);
+ } else {
+ // Generic conversion to relative - can also flatten the hierarchy
+ convertAnyToRelative(rootEdit);
+ // This already handles removing undefined layout attributes -- right?
+ //removeUndefinedLayoutAttrs(rootEdit, layout);
+ }
} else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) {
convertRelativeToLinear(rootEdit);
+ removeUndefinedLayoutAttrs(rootEdit, layout);
} else if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_TABLE_LAYOUT)) {
convertLinearToTable(rootEdit);
+ removeUndefinedLayoutAttrs(rootEdit, layout);
} else {
convertGeneric(rootEdit, oldType, newType);
+ removeUndefinedLayoutAttrs(rootEdit, layout);
}
- removeUndefinedLayoutAttrs(rootEdit, layout);
-
return changes;
}
@@ -247,9 +314,9 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element child = (Element) node;
- String id = ensureHasId(rootEdit, child);
+ String id = ensureHasId(rootEdit, child, null);
if (prevId != null) {
- addAttributeDeclaration(rootEdit, child, attributePrefix,
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
ATTR_LAYOUT_BELOW, prevId);
}
prevId = id;
@@ -266,12 +333,12 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element child = (Element) node;
- String id = ensureHasId(rootEdit, child);
+ String id = ensureHasId(rootEdit, child, null);
if (prevId != null) {
- addAttributeDeclaration(rootEdit, child, attributePrefix,
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
ATTR_LAYOUT_TO_RIGHT_OF, prevId);
if (isBaselineAligned) {
- addAttributeDeclaration(rootEdit, child, attributePrefix,
+ setAttribute(rootEdit, child, ANDROID_URI, attributePrefix,
ATTR_LAYOUT_ALIGN_BASELINE, prevId);
}
}
@@ -334,7 +401,7 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
/** Removes all the unused attributes after a conversion */
private void removeUndefinedLayoutAttrs(MultiTextEdit rootEdit, Element layout) {
- ViewElementDescriptor descriptor = getLayoutDescriptor();
+ ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn);
if (descriptor == null) {
return;
}
@@ -363,23 +430,20 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
}
}
- 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;
- }
- }
- }
+ /** Hand coded conversion from any layout to a RelativeLayout */
+ private void convertAnyToRelative(MultiTextEdit rootEdit) {
+ // To perform a conversion from any other layout type, including nested conversion,
+ Element layout = getPrimaryElement();
+ CanvasViewInfo rootView = mRootView;
+ if (rootView == null) {
+ LayoutCanvas canvas = mEditor.getGraphicalEditor().getCanvasControl();
+ ViewHierarchy viewHierarchy = canvas.getViewHierarchy();
+ rootView = viewHierarchy.getRoot();
}
- return null;
+ RelativeLayoutConversionHelper helper =
+ new RelativeLayoutConversionHelper(this, layout, mFlatten, rootEdit, rootView);
+ helper.convertToRelative();
}
public static class Descriptor extends VisualRefactoringDescriptor {
@@ -407,4 +471,12 @@ public class ChangeLayoutRefactoring extends VisualRefactoring {
return null;
}
+
+ @VisibleForTesting
+ protected CanvasViewInfo mRootView;
+
+ @VisibleForTesting
+ public void setRootView(CanvasViewInfo rootView) {
+ mRootView = rootView;
+ }
}
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
index 98066fd..b182ea4 100644
--- 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
@@ -16,24 +16,29 @@
package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_RELATIVE_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+
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.Button;
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;
+import java.util.List;
- public ChangeLayoutWizard(ChangeLayoutRefactoring ref, IProject project) {
- super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
- mProject = project;
+class ChangeLayoutWizard extends VisualRefactoringWizard {
+
+ public ChangeLayoutWizard(ChangeLayoutRefactoring ref, LayoutEditor editor) {
+ super(ref, editor);
setDefaultPageTitle("Change Layout");
}
@@ -41,7 +46,7 @@ class ChangeLayoutWizard extends RefactoringWizard {
protected void addUserInputPages() {
ChangeLayoutRefactoring ref = (ChangeLayoutRefactoring) getRefactoring();
String oldType = ref.getOldType();
- addPage(new InputPage(mProject, oldType));
+ addPage(new InputPage(mEditor.getProject(), oldType));
}
/** Wizard page which inputs parameters for the {@link ChangeLayoutRefactoring} operation */
@@ -49,6 +54,8 @@ class ChangeLayoutWizard extends RefactoringWizard {
private final IProject mProject;
private final String mOldType;
private Combo mTypeCombo;
+ private Button mFlatten;
+ private List<String> mClassNames;
public InputPage(IProject project, String oldType) {
super("ChangeLayoutInputPage"); //$NON-NLS-1$
@@ -60,6 +67,11 @@ class ChangeLayoutWizard extends RefactoringWizard {
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
+ Label fromLabel = new Label(composite, SWT.NONE);
+ fromLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ String oldTypeBase = mOldType.substring(mOldType.lastIndexOf('.') + 1);
+ fromLabel.setText(String.format("Change from %1$s", oldTypeBase));
+
Label typeLabel = new Label(composite, SWT.NONE);
typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
typeLabel.setText("New Layout Type:");
@@ -70,11 +82,40 @@ class ChangeLayoutWizard extends RefactoringWizard {
@Override
public void widgetSelected(SelectionEvent e) {
validatePage();
+ // Hierarchy flattening only works for relative layout (and any future
+ // layouts that can also support arbitrary layouts).
+ mFlatten.setVisible(mTypeCombo.getText().equals(FQCN_RELATIVE_LAYOUT));
}
};
mTypeCombo.addSelectionListener(selectionListener);
- WrapInWizard.addLayouts(mProject, mTypeCombo, mOldType);
+ mFlatten = new Button(composite, SWT.CHECK);
+ mFlatten.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mFlatten.setText("Flatten hierarchy");
+ mFlatten.addSelectionListener(selectionListener);
+ // Should flattening be selected by default?
+ mFlatten.setSelection(true);
+
+ // We don't exclude RelativeLayout even if the current layout is RelativeLayout,
+ // in case you are trying to flatten the hierarchy for a hierarchy that has a
+ // RelativeLayout at the root.
+ boolean oldIsRelativeLayout = mOldType.equals(FQCN_RELATIVE_LAYOUT);
+ String exclude = oldIsRelativeLayout ? null : mOldType;
+
+ mClassNames = WrapInWizard.addLayouts(mProject, mOldType, mTypeCombo, exclude, false);
+
+ mTypeCombo.select(0);
+ // The default should be Relative layout, if available (and not the old Type)
+ if (!oldIsRelativeLayout) {
+ for (int i = 0; i < mTypeCombo.getItemCount(); i++) {
+ if (mTypeCombo.getItem(i).equals(RELATIVE_LAYOUT)) {
+ mTypeCombo.select(i);
+ break;
+ }
+ }
+ }
+ mFlatten.setVisible(mTypeCombo.getText().equals(RELATIVE_LAYOUT));
setControl(composite);
validatePage();
@@ -83,18 +124,19 @@ class ChangeLayoutWizard extends RefactoringWizard {
private boolean validatePage() {
boolean ok = true;
- if (mTypeCombo.getText().equals(WrapInWizard.SEPARATOR_LABEL)) {
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+ if (type == null) {
setErrorMessage("Select a layout type");
- ok = false;
- }
-
- if (ok) {
+ ok = false; // The user has chosen a separator
+ } else {
setErrorMessage(null);
// Record state
ChangeLayoutRefactoring refactoring =
(ChangeLayoutRefactoring) getRefactoring();
- refactoring.setType(mTypeCombo.getText());
+ refactoring.setType(type);
+ refactoring.setFlatten(mFlatten.getSelection());
}
setPageComplete(ok);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.java
new file mode 100644
index 0000000..1d9ea86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewAction.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 "Change View Type" menu item is invoked.
+ */
+public class ChangeViewAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ChangeViewRefactoring ref = new ChangeViewRefactoring(mFile, mEditor,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ChangeViewWizard(ref, mEditor);
+ 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 Widget Type...", editor, ChangeViewAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.java
new file mode 100644
index 0000000..7705ed8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewContribution.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 ChangeViewContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ChangeViewRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ChangeViewRefactoring.Descriptor) {
+ return ((ChangeViewRefactoring.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/ChangeViewRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java
new file mode 100644
index 0000000..181f413
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoring.java
@@ -0,0 +1,254 @@
+/*
+ * 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_LAYOUT_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT;
+import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML;
+
+import com.android.annotations.VisibleForTesting;
+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.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.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.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Changes the type of the given widgets to the given target type
+ * and updates the attributes if necessary
+ */
+@SuppressWarnings("restriction") // XML model
+public class ChangeViewRefactoring 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.
+ */
+ ChangeViewRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ }
+
+ public ChangeViewRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ @VisibleForTesting
+ ChangeViewRefactoring(List<Element> selectedElements, LayoutEditor editor) {
+ super(selectedElements, editor);
+ }
+
+ @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 convert");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+ }
+
+ // Ensures that we have a valid DOM model:
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing 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 Widget Type";
+ }
+
+ 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);
+
+ for (Element element : getElements()) {
+ IndexedRegion region = getRegion(element);
+ String text = getText(region.getStartOffset(), region.getEndOffset());
+ String oldName = element.getNodeName();
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+
+ if (open != -1 && close != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + open,
+ oldLength, name));
+ if (close != open) { // Gracefully handle <FooLayout/>
+ rootEdit.addChild(new ReplaceEdit(region.getStartOffset() + close, oldLength,
+ name));
+ }
+ }
+
+ // Change tag type
+ ensureIdMatchesType(element, mTypeFqcn, rootEdit);
+
+ // Strip out attributes that no longer make sense
+ removeUndefinedAttrs(rootEdit, element);
+ }
+
+ return changes;
+ }
+
+ /** Removes all the unused attributes after a conversion */
+ private void removeUndefinedAttrs(MultiTextEdit rootEdit, Element element) {
+ ViewElementDescriptor descriptor = getElementDescriptor(mTypeFqcn);
+ if (descriptor == null) {
+ return;
+ }
+
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] layoutAttributes = descriptor.getAttributes();
+ for (AttributeDescriptor attribute : layoutAttributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ List<Attr> attributes = findAttributes(element);
+ for (Attr attribute : attributes) {
+ String name = attribute.getLocalName();
+ if (!defined.contains(name)) {
+ // Remove it
+ removeAttribute(rootEdit, element, attribute.getNamespaceURI(), name);
+ }
+ }
+
+ // Set text attribute if it's defined
+ if (defined.contains(ATTR_TEXT) && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
+ setAttribute(rootEdit, element, ANDROID_URI, getAndroidNamespacePrefix(),
+ ATTR_TEXT, descriptor.getUiName());
+ }
+ }
+
+ protected List<Attr> findAttributes(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;
+ }
+
+ List<String> getOldTypes() {
+ List<String> types = new ArrayList<String>();
+ for (Element primary : getElements()) {
+ String oldType = primary.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ types.add(oldType);
+ }
+
+ return types;
+ }
+
+ 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.changeview", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ChangeViewRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java
new file mode 100644
index 0000000..a1e2435
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewWizard.java
@@ -0,0 +1,200 @@
+/*
+ * 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 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.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IProject;
+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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ChangeViewWizard extends VisualRefactoringWizard {
+ private static final String SEPARATOR_LABEL =
+ "----------------------------------------"; //$NON-NLS-1$
+
+ public ChangeViewWizard(ChangeViewRefactoring ref, LayoutEditor editor) {
+ super(ref, editor);
+ setDefaultPageTitle("Change Widget Type");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ChangeViewRefactoring ref = (ChangeViewRefactoring) getRefactoring();
+ List<String> oldTypes = ref.getOldTypes();
+ String oldType = null;
+ for (String type : oldTypes) {
+ if (oldType == null) {
+ oldType = type;
+ } else if (!oldType.equals(type)) {
+ // If the types differ, don't offer related categories
+ oldType = null;
+ break;
+ }
+ }
+ addPage(new InputPage(mEditor.getProject(), oldType));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ChangeViewRefactoring} operation */
+ private static class InputPage extends UserInputWizardPage {
+ private final IProject mProject;
+ private Combo mTypeCombo;
+ private final String mOldType;
+ private List<String> mClassNames;
+
+ public InputPage(IProject project, String oldType) {
+ super("ChangeViewInputPage"); //$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 Widget 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);
+
+ mClassNames = getWidgetTypes(mOldType, mTypeCombo);
+ mTypeCombo.select(0);
+
+ setControl(composite);
+ validatePage();
+
+ mTypeCombo.setFocus();
+ }
+
+ private List<String> getWidgetTypes(String oldType, Combo combo) {
+ List<String> classNames = new ArrayList<String>();
+
+ // Populate type combo
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mProject);
+ if (target != null) {
+ // Try to pick "related" widgets to the one you have selected.
+ // For example, for an AnalogClock, display DigitalClock first.
+ // For a Text, offer EditText, AutoComplete, etc.
+ if (oldType != null) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ List<String> relatedTo = repository.getRelatedTo(oldType);
+ if (relatedTo.size() > 0) {
+ for (String className : relatedTo) {
+ String base = className.substring(className.lastIndexOf('.') + 1);
+ combo.add(base);
+ classNames.add(className);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+ }
+
+ Pair<List<String>,List<String>> result =
+ WrapInWizard.findViews(mProject, false);
+ List<String> customViews = result.getFirst();
+ List<String> thirdPartyViews = result.getSecond();
+ if (customViews.size() > 0) {
+ for (String view : customViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ if (thirdPartyViews.size() > 0) {
+ for (String view : thirdPartyViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ // Now add ALL known layout descriptors in case the user has
+ // a special case
+ List<ViewElementDescriptor> descriptors =
+ targetData.getLayoutDescriptors().getViewDescriptors();
+ for (ViewElementDescriptor d : descriptors) {
+ String className = d.getFullClassName();
+ if (className.equals(LayoutDescriptors.VIEW_INCLUDE)) {
+ continue;
+ }
+ combo.add(d.getUiName());
+ classNames.add(className);
+
+ }
+ }
+ }
+ } else {
+ combo.add("SDK not initialized");
+ classNames.add(null);
+ }
+
+ return classNames;
+ }
+
+ private boolean validatePage() {
+ boolean ok = true;
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+ if (type == null) {
+ setErrorMessage("Select a widget type to convert to");
+ ok = false; // The user has chosen a separator
+ } else {
+ setErrorMessage(null);
+ }
+
+ // Record state
+ ChangeViewRefactoring refactoring =
+ (ChangeViewRefactoring) getRefactoring();
+ refactoring.setType(type);
+
+ 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
index 5bd9bde..09cb044 100644
--- 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
@@ -31,7 +31,7 @@ public class ExtractIncludeAction extends VisualRefactoringAction {
if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
ExtractIncludeRefactoring ref = new ExtractIncludeRefactoring(mFile, mEditor,
mTextSelection, mTreeSelection);
- RefactoringWizard wizard = new ExtractIncludeWizard(ref, mFile.getProject());
+ RefactoringWizard wizard = new ExtractIncludeWizard(ref, mEditor);
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
index fcd6ff6..cb2a25c 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
@@ -31,8 +31,9 @@ import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttr
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.annotations.VisibleForTesting;
import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
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;
@@ -105,6 +106,11 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
super(file, editor, selection, treeSelection);
}
+ @VisibleForTesting
+ ExtractIncludeRefactoring(List<Element> selectedElements, LayoutEditor editor) {
+ super(selectedElements, editor);
+ }
+
@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
@@ -149,7 +155,6 @@ public class ExtractIncludeRefactoring extends VisualRefactoring {
}
// This also ensures that we have a valid DOM model:
- mElements = getElements();
if (mElements.size() == 0) {
status.addFatalError("Nothing to extract");
return status;
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
index a0d6c5a..403c753 100644
--- 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
@@ -17,10 +17,10 @@
package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
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;
@@ -32,12 +32,9 @@ 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;
+class ExtractIncludeWizard extends VisualRefactoringWizard {
+ public ExtractIncludeWizard(ExtractIncludeRefactoring ref, LayoutEditor editor) {
+ super(ref, editor);
setDefaultPageTitle(ref.getName());
}
@@ -45,7 +42,7 @@ class ExtractIncludeWizard extends RefactoringWizard {
protected void addUserInputPages() {
ExtractIncludeRefactoring ref = (ExtractIncludeRefactoring) getRefactoring();
String initialName = ref.getInitialName();
- addPage(new InputPage(mProject, initialName));
+ addPage(new InputPage(mEditor.getProject(), initialName));
}
/** Wizard page which inputs parameters for the {@link ExtractIncludeRefactoring} operation */
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
new file mode 100644
index 0000000..8834210
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java
@@ -0,0 +1,1690 @@
+/*
+ * 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_BACKGROUND;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_BOTTOM;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_HORIZONTAL;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_VERTICAL;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_RIGHT;
+import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_TOP;
+import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DIP;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.util.Pair;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+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;
+
+/**
+ * Helper class which performs the bulk of the layout conversion to relative layout
+ * <p>
+ * Future enhancements:
+ * <ul>
+ * <li>Render the layout at multiple screen sizes and analyze how the widgets move and
+ * stretch and use that to add in additional constraints
+ * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well
+ * (just need to tweak the "isVertical" interpretation to account for the different defaults,
+ * and perhaps do something about column size properties.
+ * <li> We need to take into account existing margins and clear/update them
+ * </ul>
+ */
+class RelativeLayoutConversionHelper {
+ private final MultiTextEdit mRootEdit;
+ private final boolean mFlatten;
+ private final Element mLayout;
+ private final ChangeLayoutRefactoring mRefactoring;
+ private final CanvasViewInfo mRootView;
+
+ RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring,
+ Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
+ mRefactoring = refactoring;
+ mLayout = layout;
+ mFlatten = flatten;
+ mRootEdit = rootEdit;
+ mRootView = rootView;
+ }
+
+ /** Performs conversion from any layout to a RelativeLayout */
+ public void convertToRelative() {
+ // Locate the view for the layout
+ CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
+ if (layoutView == null || layoutView.getChildren().size() == 0) {
+ // No children. THAT was an easy conversion!
+ return;
+ }
+
+ // Study the layout and get information about how to place individual elements
+ List<View> views = analyzeLayout(layoutView);
+
+ // Create/update relative layout constraints
+ createAttachments(views);
+ }
+
+ /**
+ * Analyzes the given view hierarchy and produces a list of {@link View} objects which
+ * contain placement information for each element
+ */
+ private List<View> analyzeLayout(CanvasViewInfo layoutView) {
+ EdgeList edgeList = new EdgeList(layoutView);
+ deleteRemovedElements(edgeList.getDeletedElements());
+
+ List<Integer> columnOffsets = edgeList.getColumnOffsets();
+ List<Integer> rowOffsets = edgeList.getRowOffsets();
+
+ // Compute x/y offsets for each row/column index
+ int[] left = new int[columnOffsets.size()];
+ int[] top = new int[rowOffsets.size()];
+
+ Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>();
+ int columnIndex = 0;
+ for (Integer offset : columnOffsets) {
+ left[columnIndex] = offset;
+ xToCol.put(offset, columnIndex++);
+ }
+ Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>();
+ int rowIndex = 0;
+ for (Integer offset : rowOffsets) {
+ top[rowIndex] = offset;
+ yToRow.put(offset, rowIndex++);
+ }
+
+ // Create a complete list of view objects
+ List<View> views = createViews(edgeList, columnOffsets);
+ initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow);
+
+ // Sanity check
+ for (View view : views) {
+ assert view.getLeftEdge() == left[view.mCol];
+ assert view.getTopEdge() == top[view.mRow];
+ assert view.getRightEdge() == left[view.mCol+view.mColSpan];
+ assert view.getBottomEdge() == top[view.mRow+view.mRowSpan];
+ }
+
+ // Ensure that every view has a proper id such that it can be referred to
+ // with a constraint
+ initializeIds(edgeList, views);
+
+ // Attempt to lay the views out in a grid with constraints (though not that widgets
+ // can overlap as well)
+ Grid grid = new Grid(views, left, top);
+ computeKnownConstraints(views, edgeList);
+ computeHorizontalConstraints(grid);
+ computeVerticalConstraints(grid);
+
+ return views;
+ }
+
+ /** Produces a list of {@link View} objects from an {@link EdgeList} */
+ private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) {
+ List<View> views = new ArrayList<View>();
+ for (Integer offset : columnOffsets) {
+ List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
+ if (leftEdgeViews == null) {
+ // must have been a right edge
+ continue;
+ }
+ for (View view : leftEdgeViews) {
+ views.add(view);
+ }
+ }
+ return views;
+ }
+
+ /** Removes any elements targeted for deletion */
+ private void deleteRemovedElements(List<Element> delete) {
+ if (mFlatten && delete.size() > 0) {
+ for (Element element : delete) {
+ mRefactoring.removeElementTags(mRootEdit, element, delete);
+ }
+ }
+ }
+
+ /** Ensures that every element has an id such that it can be referenced from a constraint */
+ private void initializeIds(EdgeList edgeList, List<View> views) {
+ // Ensure that all views have a valid id
+ for (View view : views) {
+ String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null);
+ edgeList.setIdAttributeValue(view, id);
+ }
+ }
+
+ /**
+ * Initializes the column and row indices, as well as any column span and row span
+ * values
+ */
+ private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets,
+ List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) {
+ // Now initialize table view row, column and spans
+ for (Integer offset : columnOffsets) {
+ List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
+ if (leftEdgeViews == null) {
+ // must have been a right edge
+ continue;
+ }
+ for (View view : leftEdgeViews) {
+ Integer col = xToCol.get(view.getLeftEdge());
+ assert col != null;
+ Integer end = xToCol.get(view.getRightEdge());
+ assert end != null;
+
+ view.mCol = col;
+ view.mColSpan = end - col;
+ }
+ }
+
+ for (Integer offset : rowOffsets) {
+ List<View> topEdgeViews = edgeList.getTopEdgeViews(offset);
+ if (topEdgeViews == null) {
+ // must have been a bottom edge
+ continue;
+ }
+ for (View view : topEdgeViews) {
+ Integer row = yToRow.get(view.getTopEdge());
+ assert row != null;
+ Integer end = yToRow.get(view.getBottomEdge());
+ assert end != null;
+
+ view.mRow = row;
+ view.mRowSpan = end - row;
+ }
+ }
+ }
+
+ /**
+ * Creates refactoring edits which adds or updates constraints for the given list of
+ * views
+ */
+ private void createAttachments(List<View> views) {
+ // Make the attachments
+ String namespace = mRefactoring.getAndroidNamespacePrefix();
+ for (View view : views) {
+ for (Pair<String, String> constraint : view.getHorizConstraints()) {
+ mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
+ namespace, constraint.getFirst(), constraint.getSecond());
+ }
+ for (Pair<String, String> constraint : view.getVerticalConstraints()) {
+ mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
+ namespace, constraint.getFirst(), constraint.getSecond());
+ }
+ }
+ }
+
+ /**
+ * Analyzes the existing layouts and layout parameter objects in the document to infer
+ * constraints for layout types that we know about - such as LinearLayout baseline
+ * alignment, weights, gravity, etc.
+ */
+ private void computeKnownConstraints(List<View> views, EdgeList edgeList) {
+ // List of parent layout elements we've already processed. We iterate through all
+ // the -children-, and we ask each for its element parent (which won't have a view)
+ // and we look at the parent's layout attributes and its children layout constraints,
+ // and then we stash away constraints that we can infer. This means that we will
+ // encounter the same parent for every sibling, so that's why there's a map to
+ // prevent duplicate work.
+ Set<Node> seen = new HashSet<Node>();
+
+ for (View view : views) {
+ Element element = view.getElement();
+ Node parent = element.getParentNode();
+ if (seen.contains(parent)) {
+ continue;
+ }
+ seen.add(parent);
+
+ if (parent.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ Element layout = (Element) parent;
+ String layoutName = layout.getTagName();
+
+ if (LINEAR_LAYOUT.equals(layoutName)) {
+ analyzeLinearLayout(edgeList, layout);
+ } else if (RELATIVE_LAYOUT.equals(layoutName)) {
+ analyzeRelativeLayout(edgeList, layout);
+ } else {
+ // Some other layout -- add more conditional handling here
+ // for framelayout, tables, etc.
+ }
+ }
+ }
+
+ private static final int GRAVITY_LEFT = 1 << 0;
+ private static final int GRAVITY_RIGHT = 1<< 1;
+ private static final int GRAVITY_CENTER_HORIZ = 1 << 2;
+ private static final int GRAVITY_FILL_HORIZ = 1 << 3;
+ private static final int GRAVITY_CENTER_VERT = 1 << 4;
+ private static final int GRAVITY_FILL_VERT = 1 << 5;
+ private static final int GRAVITY_TOP = 1 << 6;
+ private static final int GRAVITY_BOTTOM = 1 << 7;
+ private static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ
+ | GRAVITY_LEFT | GRAVITY_RIGHT;
+ private static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT
+ | GRAVITY_TOP | GRAVITY_BOTTOM;
+
+ /** Returns the gravity of the given element */
+ private static int getGravity(Element element) {
+ int gravity = GRAVITY_LEFT | GRAVITY_TOP;
+ String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
+ if (gravityString != null && gravityString.length() > 0) {
+ String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$
+ for (String anchor : anchors) {
+ if (GRAVITY_VALUE_CENTER.equals(anchor)) {
+ gravity = GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT;
+ } else if (GRAVITY_VALUE_FILL.equals(anchor)) {
+ gravity = GRAVITY_FILL_HORIZ | GRAVITY_FILL_VERT;
+ } else if (GRAVITY_VALUE_CENTER_VERTICAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_CENTER_VERT;
+ } else if (GRAVITY_VALUE_CENTER_HORIZONTAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_CENTER_HORIZ;
+ } else if (GRAVITY_VALUE_FILL_VERTICAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_FILL_VERT;
+ } else if (GRAVITY_VALUE_FILL_HORIZONTAL.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_FILL_HORIZ;
+ } else if (GRAVITY_VALUE_TOP.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_TOP;
+ } else if (GRAVITY_VALUE_BOTTOM.equals(anchor)) {
+ gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_BOTTOM;
+ } else if (GRAVITY_VALUE_LEFT.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_LEFT;
+ } else if (GRAVITY_VALUE_RIGHT.equals(anchor)) {
+ gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_RIGHT;
+ } else {
+ // "clip" not supported
+ }
+ }
+ }
+
+ return gravity;
+ }
+
+ /**
+ * Returns the element children of the given element
+ *
+ * @param element the parent element
+ * @return a list of child elements, possibly empty but never null
+ */
+ public static List<Element> getChildren(Element element) {
+ // Convenience to avoid lots of ugly DOM access casting
+ NodeList children = element.getChildNodes();
+ // An iterator would have been more natural (to directly drive the child list
+ // iteration) but iterators can't be used in enhanced for loops...
+ List<Element> result = new ArrayList<Element>(children.getLength());
+ 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;
+ result.add(child);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
+ * does not define a weight
+ */
+ private float getWeight(Element linearLayoutChild) {
+ String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
+ if (weight != null && weight.length() > 0) {
+ try {
+ return Float.parseFloat(weight);
+ } catch (NumberFormatException nfe) {
+ AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
+ }
+ }
+
+ return 0.0f;
+ }
+
+ /**
+ * Returns the sum of all the layout weights of the children in the given LinearLayout
+ *
+ * @param linearLayout the layout to compute the total sum for
+ * @return the total sum of all the layout weights in the given layout
+ */
+ private float getWeightSum(Element linearLayout) {
+ float sum = 0;
+ for (Element child : getChildren(linearLayout)) {
+ sum += getWeight(child);
+ }
+
+ return sum;
+ }
+
+ /**
+ * Analyzes the given LinearLayout and updates the constraints to reflect
+ * relationships it can infer - based on baseline alignment, gravity, order and
+ * weights. This method also removes "0dip" as a special width/height used in
+ * LinearLayouts with weight distribution.
+ */
+ private void analyzeLinearLayout(EdgeList edgeList, Element layout) {
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+ View baselineRef = null;
+ if (!isVertical &&
+ !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) {
+ // Baseline alignment. Find the tallest child and set it as the baseline reference.
+ int tallestHeight = 0;
+ View tallest = null;
+ for (Element child : getChildren(layout)) {
+ View view = edgeList.getView(child);
+ if (view != null && view.getHeight() > tallestHeight) {
+ tallestHeight = view.getHeight();
+ tallest = view;
+ }
+ }
+ if (tallest != null) {
+ baselineRef = tallest;
+ }
+ }
+
+ float weightSum = getWeightSum(layout);
+ float cumulativeWeight = 0;
+
+ List<Element> children = getChildren(layout);
+ String prevId = null;
+ boolean isFirstChild = true;
+ boolean linkBackwards = true;
+ boolean linkForwards = false;
+
+ for (int index = 0, childCount = children.size(); index < childCount; index++) {
+ Element child = children.get(index);
+
+ View childView = edgeList.getView(child);
+ if (childView == null) {
+ // Could be a nested layout that is being removed etc
+ prevId = null;
+ isFirstChild = false;
+ continue;
+ }
+
+ // Look at the layout_weight attributes and determine whether we should be
+ // attached on the bottom/right or on the top/left
+ if (weightSum > 0.0f) {
+ float weight = getWeight(child);
+
+ // We can't emulate a LinearLayout where multiple children have positive
+ // weights. However, we CAN support the common scenario where a single
+ // child has a non-zero weight, and all children after it are pushed
+ // to the end and the weighted child fills the remaining space.
+ if (cumulativeWeight == 0 && weight > 0) {
+ // See if we have a bottom/right edge to attach the forwards link to
+ // (at the end of the forwards chains). Only if so can we link forwards.
+ View referenced;
+ if (isVertical) {
+ referenced = edgeList.getSharedBottomEdge(layout);
+ } else {
+ referenced = edgeList.getSharedRightEdge(layout);
+ }
+ if (referenced != null) {
+ linkForwards = true;
+ }
+ } else if (cumulativeWeight > 0) {
+ linkBackwards = false;
+ }
+
+ cumulativeWeight += weight;
+ }
+
+ analyzeGravity(edgeList, layout, isVertical, child, childView);
+ convert0dipToWrapContent(child);
+
+ // Chain elements together in the flow direction of the linear layout
+ if (prevId != null) { // No constraint for first child
+ if (linkBackwards) {
+ if (isVertical) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId);
+ }
+ }
+ } else if (isFirstChild) {
+ assert linkBackwards;
+
+ // First element; attach it to the parent if we can
+ if (isVertical) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ }
+ } else {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
+ }
+ }
+ }
+ }
+
+ if (linkForwards) {
+ if (index < (childCount - 1)) {
+ Element nextChild = children.get(index + 1);
+ String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null);
+ if (nextId != null) {
+ if (isVertical) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId);
+ }
+ }
+ } else {
+ // Attach to right/bottom edge of the layout
+ if (isVertical) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ }
+ } else {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
+ }
+ }
+ }
+ }
+ }
+
+ if (baselineRef != null && !baselineRef.equals(childView.getId())) {
+ assert !isVertical;
+ // Only align if they share the same gravity
+ if ((childView.getGravity() & GRAVITY_VERT_MASK) ==
+ (baselineRef.getGravity() & GRAVITY_VERT_MASK)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId());
+ }
+ }
+
+ prevId = mRefactoring.ensureHasId(mRootEdit, child, null);
+ isFirstChild = false;
+ }
+ }
+
+ /**
+ * Checks the layout "gravity" value for the given child and updates the constraints
+ * to account for the gravity
+ */
+ private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical,
+ Element child, View childView) {
+ // Use gravity to constrain elements in the axis orthogonal to the
+ // direction of the layout
+ int gravity = childView.getGravity();
+ if (isVertical) {
+ if ((gravity & GRAVITY_RIGHT) != 0) {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
+ referenced.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) {
+ View referenced1 = edgeList.getSharedLeftEdge(layout);
+ View referenced2 = edgeList.getSharedRightEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL,
+ VALUE_TRUE);
+ }
+ }
+ } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) {
+ View referenced1 = edgeList.getSharedLeftEdge(layout);
+ View referenced2 = edgeList.getSharedRightEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
+ referenced1.getId());
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
+ referenced2.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_LEFT) != 0) {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
+ referenced.getId());
+ }
+ }
+ }
+ } else {
+ // Handle horizontal layout: perform vertical gravity attachments
+ if ((gravity & GRAVITY_BOTTOM) != 0) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_CENTER_VERT) != 0) {
+ View referenced1 = edgeList.getSharedTopEdge(layout);
+ View referenced2 = edgeList.getSharedBottomEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL,
+ VALUE_TRUE);
+ }
+ }
+ } else if ((gravity & GRAVITY_FILL_VERT) != 0) {
+ View referenced1 = edgeList.getSharedTopEdge(layout);
+ View referenced2 = edgeList.getSharedBottomEdge(layout);
+ if (referenced1 != null && referenced2 == referenced1) {
+ if (isAncestor(referenced1.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced1.getId());
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced2.getId());
+ }
+ }
+ } else if ((gravity & GRAVITY_TOP) != 0) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ }
+ }
+ }
+ return gravity;
+ }
+
+ /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
+ private void convert0dipToWrapContent(Element child) {
+ // Must convert layout_height="0dip" to layout_height="wrap_content".
+ // 0dip is a special trick used in linear layouts in the presence of
+ // weights where 0dip ensures that the height of the view is not taken
+ // into account when distributing the weights. However, when converted
+ // to RelativeLayout this will instead cause the view to actually be assigned
+ // 0 height.
+ String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ // 0dip, 0dp, 0px, etc
+ if (height != null && height.startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
+ mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT,
+ VALUE_WRAP_CONTENT);
+ }
+ String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ if (width != null && width.startsWith("0")) { //$NON-NLS-1$
+ mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
+ mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH,
+ VALUE_WRAP_CONTENT);
+ }
+ }
+
+ /**
+ * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the
+ * constraints in the EdgeList with those relationships which can continue in the
+ * outer single RelativeLayout.
+ */
+ private void analyzeRelativeLayout(EdgeList edgeList, Element 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;
+ View childView = edgeList.getView(child);
+ if (childView == null) {
+ // Could be a nested layout that is being removed etc
+ continue;
+ }
+
+ NamedNodeMap attributes = child.getAttributes();
+ for (int j = 0, m = attributes.getLength(); j < m; j++) {
+ Attr attribute = (Attr) attributes.item(j);
+ String name = attribute.getLocalName();
+ String value = attribute.getValue();
+ if (name.equals(ATTR_LAYOUT_WIDTH)
+ || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Ignore these for now
+ } else if (name.startsWith(ATTR_LAYOUT_PREFIX)
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Determine if the reference is to a known edge
+ String id = getIdBasename(value);
+ if (id != null) {
+ View referenced = edgeList.getView(id);
+ if (referenced != null) {
+ // This is a valid reference, so preserve
+ // the attribute
+ if (name.equals(ATTR_LAYOUT_BELOW) ||
+ name.equals(ATTR_LAYOUT_ABOVE) ||
+ name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
+ name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) {
+ // Vertical constraint
+ childView.addVerticalConstraint(name, value);
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
+ name.equals(ATTR_LAYOUT_TO_LEFT_OF) ||
+ name.equals(ATTR_LAYOUT_TO_RIGHT_OF) ||
+ name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) {
+ // Horizontal constraint
+ childView.addHorizConstraint(name, value);
+ } else {
+ // We don't expect this
+ assert false : name;
+ }
+ } else {
+ // Reference to some layout that is not included here.
+ // TODO: See if the given layout has an edge
+ // that corresponds to one of our known views
+ // so we can adjust the constraints and keep it after all.
+ }
+ } else {
+ // It's a parent-relative constraint (such
+ // as aligning with a parent edge, or centering
+ // in the parent view)
+ boolean remove = true;
+ if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
+ View referenced = edgeList.getSharedLeftEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
+ View referenced = edgeList.getSharedRightEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addHorizConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addHorizConstraint(
+ ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
+ View referenced = edgeList.getSharedTopEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
+ referenced.getId());
+ }
+ remove = false;
+ }
+ } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
+ View referenced = edgeList.getSharedBottomEdge(layout);
+ if (referenced != null) {
+ if (isAncestor(referenced.getElement(), child)) {
+ childView.addVerticalConstraint(name, VALUE_TRUE);
+ } else {
+ childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
+ referenced.getId());
+ }
+ remove = false;
+ }
+ }
+
+ boolean alignWithParent =
+ name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING);
+ if (remove && alignWithParent) {
+ // TODO - look for this one AFTER we have processed
+ // everything else, and then set constraints as necessary
+ // IF there are no other conflicting constraints!
+ }
+
+ // Otherwise it's some kind of centering which we don't support
+ // yet.
+
+ // TODO: Find a way to determine whether we have
+ // a corresponding edge for the parent (e.g. if
+ // the ViewInfo bounds match our outer parent or
+ // some other edge) and if so, substitute for that
+ // id.
+ // For example, if this element was centered
+ // horizontally in a RelativeLayout that actually
+ // occupies the entire width of our outer layout,
+ // then it can be preserved after all!
+
+ if (remove) {
+ if (name.startsWith("layout_margin")) { //$NON-NLS-1$
+ continue;
+ }
+
+ // Remove unknown attributes?
+ // It's too early to do this, because we may later want
+ // to *set* this value and it would result in an overlapping edits
+ // exception. Therefore, we need to RECORD which attributes should
+ // be removed, which lines should have its indentation adjusted
+ // etc and finally process it all at the end!
+ //mRefactoring.removeAttribute(mRootEdit, child,
+ // attribute.getNamespaceURI(), name);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will
+ * return null.
+ */
+ private static String getIdBasename(String id) {
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ return id.substring(NEW_ID_PREFIX.length());
+ } else if (id.startsWith(ID_PREFIX)) {
+ return id.substring(ID_PREFIX.length());
+ }
+
+ return null;
+ }
+
+ /** Returns true if the given second argument is a descendant of the first argument */
+ private static boolean isAncestor(Node ancestor, Node node) {
+ while (node != null) {
+ if (node == ancestor) {
+ return true;
+ }
+ node = node.getParentNode();
+ }
+ return false;
+ }
+
+ /**
+ * Computes horizontal constraints for the views in the grid for any remaining views
+ * that do not have constraints (as the result of the analysis of known layouts). This
+ * will look at the rendered layout coordinates and attempt to connect elements based
+ * on a spatial layout in the grid.
+ */
+ private void computeHorizontalConstraints(Grid grid) {
+ int columns = grid.getColumns();
+
+ String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT;
+ String attachLeftValue = VALUE_TRUE;
+ int marginLeft = 0;
+ for (int col = 0; col < columns; col++) {
+ if (!grid.colContainsTopLeftCorner(col)) {
+ // Just accumulate margins for the next column
+ marginLeft += grid.getColumnWidth(col);
+ } else {
+ // Add horizontal attachments
+ String firstId = null;
+ for (View view : grid.viewsStartingInCol(col, true)) {
+ assert view.getId() != null;
+ if (firstId == null) {
+ firstId = view.getId();
+ if (view.isConstrainedHorizontally()) {
+ // Nothing to do -- we already have an accurate position for
+ // this view
+ } else if (attachLeftProperty != null) {
+ view.addHorizConstraint(attachLeftProperty, attachLeftValue);
+ if (marginLeft > 0) {
+ view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT,
+ String.format(VALUE_N_DIP, marginLeft));
+ marginLeft = 0;
+ }
+ } else {
+ assert false;
+ }
+ } else if (!view.isConstrainedHorizontally()) {
+ view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId);
+ }
+ }
+ }
+
+ // Figure out edge for the next column
+ View view = grid.findRightEdgeView(col);
+ if (view != null) {
+ assert view.getId() != null;
+ attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF;
+ attachLeftValue = view.getId();
+
+ marginLeft = 0;
+ } else if (marginLeft == 0) {
+ marginLeft = grid.getColumnWidth(col);
+ }
+ }
+ }
+
+ /**
+ * Performs vertical layout just like the {@link #computeHorizontalConstraints} method
+ * did horizontally
+ */
+ private void computeVerticalConstraints(Grid grid) {
+ int rows = grid.getRows();
+
+ String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP;
+ String attachTopValue = VALUE_TRUE;
+ int marginTop = 0;
+ for (int row = 0; row < rows; row++) {
+ if (!grid.rowContainsTopLeftCorner(row)) {
+ // Just accumulate margins for the next column
+ marginTop += grid.getRowHeight(row);
+ } else {
+ // Add horizontal attachments
+ String firstId = null;
+ for (View view : grid.viewsStartingInRow(row, true)) {
+ assert view.getId() != null;
+ if (firstId == null) {
+ firstId = view.getId();
+ if (view.isConstrainedVertically()) {
+ // Nothing to do -- we already have an accurate position for
+ // this view
+ } else if (attachTopProperty != null) {
+ view.addVerticalConstraint(attachTopProperty, attachTopValue);
+ if (marginTop > 0) {
+ view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP,
+ String.format(VALUE_N_DIP, marginTop));
+ marginTop = 0;
+ }
+ } else {
+ assert false;
+ }
+ } else if (!view.isConstrainedVertically()) {
+ view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId);
+ }
+ }
+ }
+
+ // Figure out edge for the next row
+ View view = grid.findBottomEdgeView(row);
+ if (view != null) {
+ assert view.getId() != null;
+ attachTopProperty = ATTR_LAYOUT_BELOW;
+ attachTopValue = view.getId();
+ marginTop = 0;
+ } else if (marginTop == 0) {
+ marginTop = grid.getRowHeight(row);
+ }
+ }
+ }
+
+ /**
+ * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
+ * {@link Element}
+ *
+ * @param info the root {@link CanvasViewInfo} to search below
+ * @param element the target element
+ * @return the {@link CanvasViewInfo} which corresponds to the given element
+ */
+ private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
+ if (getElement(info) == element) {
+ return info;
+ }
+
+ for (CanvasViewInfo child : info.getChildren()) {
+ CanvasViewInfo result = findViewForElement(child, element);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
+ private static Element getElement(CanvasViewInfo info) {
+ Node node = info.getUiViewNode().getXmlNode();
+ if (node instanceof Element) {
+ return (Element) node;
+ }
+
+ return null;
+ }
+
+ /**
+ * A grid of cells which can contain views, used to infer spatial relationships when
+ * computing constraints. Note that a view can appear in than one cell; they will
+ * appear in all cells that their bounds overlap with!
+ */
+ private class Grid {
+ private final int[] mLeft;
+ private final int[] mTop;
+ // A list from row to column to cell, where a cell is a list of views
+ private final List<List<List<View>>> mRowList;
+ private int mRowCount;
+ private int mColCount;
+
+ Grid(List<View> views, int[] left, int[] top) {
+ mLeft = left;
+ mTop = top;
+
+ // The left/top arrays should include the ending point too
+ mColCount = left.length - 1;
+ mRowCount = top.length - 1;
+
+ // Using nested lists rather than arrays to avoid lack of typed arrays
+ // (can't create List<View>[row][column] arrays)
+ mRowList = new ArrayList<List<List<View>>>(top.length);
+ for (int row = 0; row < top.length; row++) {
+ List<List<View>> columnList = new ArrayList<List<View>>(left.length);
+ for (int col = 0; col < left.length; col++) {
+ columnList.add(new ArrayList<View>(4));
+ }
+ mRowList.add(columnList);
+ }
+
+ for (View view : views) {
+ // Get rid of the root view; we don't want that in the attachments logic;
+ // it was there originally such that it would contribute the outermost
+ // edges.
+ if (view.mElement == mLayout) {
+ continue;
+ }
+
+ for (int i = 0; i < view.mRowSpan; i++) {
+ for (int j = 0; j < view.mColSpan; j++) {
+ mRowList.get(view.mRow + i).get(view.mCol + j).add(view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of rows in the grid
+ *
+ * @return the row count
+ */
+ public int getRows() {
+ return mRowCount;
+ }
+
+ /**
+ * Returns the number of columns in the grid
+ *
+ * @return the column count
+ */
+ public int getColumns() {
+ return mColCount;
+ }
+
+ /**
+ * Returns the list of views overlapping the given cell
+ *
+ * @param row the row of the target cell
+ * @param col the column of the target cell
+ * @return a list of views overlapping the given column
+ */
+ public List<View> get(int row, int col) {
+ return mRowList.get(row).get(col);
+ }
+
+ /**
+ * Returns true if the given column contains a top left corner of a view
+ *
+ * @param column the column to check
+ * @return true if one or more views have their top left corner in this column
+ */
+ public boolean colContainsTopLeftCorner(int column) {
+ for (int row = 0; row < mRowCount; row++) {
+ View view = getTopLeftCorner(row, column);
+ if (view != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the given row contains a top left corner of a view
+ *
+ * @param row the row to check
+ * @return true if one or more views have their top left corner in this row
+ */
+ public boolean rowContainsTopLeftCorner(int row) {
+ for (int col = 0; col < mColCount; col++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a list of views (optionally sorted by increasing row index) that have
+ * their left edge starting in the given column
+ *
+ * @param col the column to look up views for
+ * @param sort whether to sort the result in increasing row order
+ * @return a list of views starting in the given column
+ */
+ public List<View> viewsStartingInCol(int col, boolean sort) {
+ List<View> views = new ArrayList<View>();
+ for (int row = 0; row < mRowCount; row++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ views.add(view);
+ }
+ }
+
+ if (sort) {
+ View.sortByRow(views);
+ }
+
+ return views;
+ }
+
+ /**
+ * Returns a list of views (optionally sorted by increasing column index) that have
+ * their top edge starting in the given row
+ *
+ * @param row the row to look up views for
+ * @param sort whether to sort the result in increasing column order
+ * @return a list of views starting in the given row
+ */
+ public List<View> viewsStartingInRow(int row, boolean sort) {
+ List<View> views = new ArrayList<View>();
+ for (int col = 0; col < mColCount; col++) {
+ View view = getTopLeftCorner(row, col);
+ if (view != null) {
+ views.add(view);
+ }
+ }
+
+ if (sort) {
+ View.sortByColumn(views);
+ }
+
+ return views;
+ }
+
+ /**
+ * Returns the pixel width of the given column
+ *
+ * @param col the column to look up the width of
+ * @return the width of the column
+ */
+ public int getColumnWidth(int col) {
+ return mLeft[col + 1] - mLeft[col];
+ }
+
+ /**
+ * Returns the pixel height of the given row
+ *
+ * @param row the row to look up the height of
+ * @return the height of the row
+ */
+ public int getRowHeight(int row) {
+ return mTop[row + 1] - mTop[row];
+ }
+
+ /**
+ * Returns the first view found that has its top left corner in the cell given by
+ * the row and column indexes, or null if not found.
+ *
+ * @param row the row of the target cell
+ * @param col the column of the target cell
+ * @return a view with its top left corner in the given cell, or null if not found
+ */
+ View getTopLeftCorner(int row, int col) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ for (View view : views) {
+ if (view.mRow == row && view.mCol == col) {
+ return view;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public View findRightEdgeView(int col) {
+ for (int row = 0; row < mRowCount; row++) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ List<View> result = new ArrayList<View>();
+ for (View view : views) {
+ // Ends on the right edge of this column?
+ if (view.mCol + view.mColSpan == col + 1) {
+ result.add(view);
+ }
+ }
+ if (result.size() > 1) {
+ View.sortByColumn(result);
+ }
+ if (result.size() > 0) {
+ return result.get(0);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public View findBottomEdgeView(int row) {
+ for (int col = 0; col < mColCount; col++) {
+ List<View> views = get(row, col);
+ if (views.size() > 0) {
+ List<View> result = new ArrayList<View>();
+ for (View view : views) {
+ // Ends on the bottom edge of this column?
+ if (view.mRow + view.mRowSpan == row + 1) {
+ result.add(view);
+ }
+ }
+ if (result.size() > 1) {
+ View.sortByRow(result);
+ }
+ if (result.size() > 0) {
+ return result.get(0);
+ }
+
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Produces a display of view contents along with the pixel positions of each row/column,
+ * like the following (used for diagnostics only)
+ * <pre>
+ * |0 |49 |143 |192 |240
+ * 36| | |button2 |
+ * 72| |radioButton1 |button2 |
+ * 74|button1 |radioButton1 |button2 |
+ * 108|button1 | |button2 |
+ * 110| | |button2 |
+ * 149| | | |
+ * 320
+ * </pre>
+ */
+ @Override
+ public String toString() {
+ // Dump out the view table
+ int cellWidth = 20;
+
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter out = new PrintWriter(stringWriter);
+ out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ for (int col = 0; col < mColCount + 1; col++) {
+ out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ for (int row = 0; row < mRowCount + 1; row++) {
+ out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
+ if (row == mRowCount) {
+ break;
+ }
+ for (int col = 0; col < mColCount; col++) {
+ List<View> views = get(row, col);
+ StringBuilder sb = new StringBuilder();
+ for (View view : views) {
+ String id = view != null ? view.getId() : ""; //$NON-NLS-1$
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ id = id.substring(NEW_ID_PREFIX.length());
+ }
+ if (id.length() > cellWidth - 2) {
+ id = id.substring(0, cellWidth - 2);
+ }
+ if (sb.length() > 0) {
+ sb.append(","); //$NON-NLS-1$
+ }
+ sb.append(id);
+ }
+ String cellString = sb.toString();
+ if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
+ cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
+ }
+ out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ out.printf("\n"); //$NON-NLS-1$
+ }
+
+ out.flush();
+ return stringWriter.toString();
+ }
+ }
+
+ /** Holds layout information about an individual view. */
+ private static class View {
+ private final Element mElement;
+ private int mRow = -1;
+ private int mCol = -1;
+ private int mRowSpan = -1;
+ private int mColSpan = -1;
+ private CanvasViewInfo mInfo;
+ private String mId;
+ private List<Pair<String, String>> mHorizConstraints =
+ new ArrayList<Pair<String, String>>(4);
+ private List<Pair<String, String>> mVerticalConstraints =
+ new ArrayList<Pair<String, String>>(4);
+ private int mGravity;
+
+ public View(CanvasViewInfo view, Element element) {
+ mInfo = view;
+ mElement = element;
+ mGravity = RelativeLayoutConversionHelper.getGravity(element);
+ }
+
+ public int getHeight() {
+ return mInfo.getAbsRect().height;
+ }
+
+ public int getGravity() {
+ return mGravity;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public Element getElement() {
+ return mElement;
+ }
+
+ public List<Pair<String, String>> getHorizConstraints() {
+ return mHorizConstraints;
+ }
+
+ public List<Pair<String, String>> getVerticalConstraints() {
+ return mVerticalConstraints;
+ }
+
+ public boolean isConstrainedHorizontally() {
+ return mHorizConstraints.size() > 0;
+ }
+
+ public boolean isConstrainedVertically() {
+ return mVerticalConstraints.size() > 0;
+ }
+
+ public void addHorizConstraint(String property, String value) {
+ assert property != null && value != null;
+ // TODO - look for duplicates?
+ mHorizConstraints.add(Pair.of(property, value));
+ }
+
+ public void addVerticalConstraint(String property, String value) {
+ assert property != null && value != null;
+ mVerticalConstraints.add(Pair.of(property, value));
+ }
+
+ public int getLeftEdge() {
+ return mInfo.getAbsRect().x;
+ }
+
+ public int getTopEdge() {
+ return mInfo.getAbsRect().y;
+ }
+
+ public int getRightEdge() {
+ Rectangle bounds = mInfo.getAbsRect();
+ // +1: make the bounds overlap, so the right edge is the same as the
+ // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide
+ // columns between adjacent items.
+ return bounds.x + bounds.width + 1;
+ }
+
+ public int getBottomEdge() {
+ Rectangle bounds = mInfo.getAbsRect();
+ return bounds.y + bounds.height + 1;
+ }
+
+ @Override
+ public String toString() {
+ return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ public static void sortByRow(List<View> views) {
+ Collections.sort(views, new ViewComparator(true/*rowSort*/));
+ }
+
+ public static void sortByColumn(List<View> views) {
+ Collections.sort(views, new ViewComparator(false/*rowSort*/));
+ }
+
+ /** Comparator to help sort views by row or column index */
+ private static class ViewComparator implements Comparator<View> {
+ boolean mRowSort;
+
+ public ViewComparator(boolean rowSort) {
+ mRowSort = rowSort;
+ }
+
+ public int compare(View view1, View view2) {
+ if (mRowSort) {
+ return view1.mRow - view2.mRow;
+ } else {
+ return view1.mCol - view2.mCol;
+ }
+ }
+ }
+ }
+
+ /**
+ * An edge list takes a hierarchy of elements and records the bounds of each element
+ * into various lists such that it can answer queries about shared edges, about which
+ * particular pixels occur as a boundary edge, etc.
+ */
+ private class EdgeList {
+ private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100);
+ private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100);
+ private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>();
+ private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>();
+ private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>();
+ private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>();
+ private final List<Element> mDelete = new ArrayList<Element>();
+
+ EdgeList(CanvasViewInfo view) {
+ analyze(view, true);
+ mDelete.remove(getElement(view));
+ }
+
+ public void setIdAttributeValue(View view, String id) {
+ assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX);
+ view.mId = id;
+ mIdToViewMap.put(getIdBasename(id), view);
+ }
+
+ public View getView(Element element) {
+ return mElementToViewMap.get(element);
+ }
+
+ public View getView(String id) {
+ return mIdToViewMap.get(id);
+ }
+
+ public List<View> getTopEdgeViews(Integer topOffset) {
+ return mTop.get(topOffset);
+ }
+
+ public List<View> getLeftEdgeViews(Integer leftOffset) {
+ return mLeft.get(leftOffset);
+ }
+
+ void record(Map<Integer, List<View>> map, Integer edge, View info) {
+ List<View> list = map.get(edge);
+ if (list == null) {
+ list = new ArrayList<View>();
+ map.put(edge, list);
+ }
+ list.add(info);
+ }
+
+ private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) {
+ Set<Integer> joined = new HashSet<Integer>(first.size() + second.size());
+ joined.addAll(first);
+ joined.addAll(second);
+ List<Integer> unique = new ArrayList<Integer>(joined);
+ Collections.sort(unique);
+
+ return unique;
+ }
+
+ public List<Element> getDeletedElements() {
+ return mDelete;
+ }
+
+ public List<Integer> getColumnOffsets() {
+ return getOffsets(mLeft.keySet(), mRight.keySet());
+ }
+ public List<Integer> getRowOffsets() {
+ return getOffsets(mTop.keySet(), mBottom.keySet());
+ }
+
+ private View analyze(CanvasViewInfo view, boolean isRoot) {
+ View added = null;
+ if (!mFlatten || !isRemovableLayout(view)) {
+ added = add(view);
+ if (!isRoot) {
+ return added;
+ }
+ } else {
+ mDelete.add(getElement(view));
+ }
+
+ Element parentElement = getElement(view);
+ Rectangle parentBounds = view.getAbsRect();
+
+ // Build up a table model of the view
+ for (CanvasViewInfo child : view.getChildren()) {
+ Rectangle childBounds = child.getAbsRect();
+ Element childElement = getElement(child);
+
+ // See if this view shares the edge with the removed
+ // parent layout, and if so, record that such that we can
+ // later handle attachments to the removed parent edges
+ if (parentBounds.x == childBounds.x) {
+ mSharedLeftEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.y == childBounds.y) {
+ mSharedTopEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) {
+ mSharedRightEdge.put(childElement, parentElement);
+ }
+ if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) {
+ mSharedBottomEdge.put(childElement, parentElement);
+ }
+
+ if (mFlatten && isRemovableLayout(child)) {
+ // When flattening, we want to disregard all layouts and instead
+ // add their children!
+ for (CanvasViewInfo childView : child.getChildren()) {
+ analyze(childView, false);
+
+ Element childViewElement = getElement(childView);
+ Rectangle childViewBounds = childView.getAbsRect();
+
+ // See if this view shares the edge with the removed
+ // parent layout, and if so, record that such that we can
+ // later handle attachments to the removed parent edges
+ if (parentBounds.x == childViewBounds.x) {
+ mSharedLeftEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.y == childViewBounds.y) {
+ mSharedTopEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.x + parentBounds.width == childViewBounds.x
+ + childViewBounds.width) {
+ mSharedRightEdge.put(childViewElement, parentElement);
+ }
+ if (parentBounds.y + parentBounds.height == childViewBounds.y
+ + childViewBounds.height) {
+ mSharedBottomEdge.put(childViewElement, parentElement);
+ }
+ }
+ mDelete.add(childElement);
+ } else {
+ analyze(child, false);
+ }
+ }
+
+ return added;
+ }
+
+ public View getSharedLeftEdge(Element element) {
+ return getSharedEdge(element, mSharedLeftEdge);
+ }
+
+ public View getSharedRightEdge(Element element) {
+ return getSharedEdge(element, mSharedRightEdge);
+ }
+
+ public View getSharedTopEdge(Element element) {
+ return getSharedEdge(element, mSharedTopEdge);
+ }
+
+ public View getSharedBottomEdge(Element element) {
+ return getSharedEdge(element, mSharedBottomEdge);
+ }
+
+ private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) {
+ Element original = element;
+
+ while (element != null) {
+ View view = getView(element);
+ if (view != null) {
+ assert isAncestor(element, original);
+ return view;
+ }
+ element = sharedEdgeMap.get(element);
+ }
+
+ return null;
+ }
+
+ private View add(CanvasViewInfo info) {
+ Rectangle bounds = info.getAbsRect();
+ Element element = getElement(info);
+ View view = new View(info, element);
+ mElementToViewMap.put(element, view);
+ record(mLeft, Integer.valueOf(bounds.x), view);
+ record(mTop, Integer.valueOf(bounds.y), view);
+ record(mRight, Integer.valueOf(view.getRightEdge()), view);
+ record(mBottom, Integer.valueOf(view.getBottomEdge()), view);
+ return view;
+ }
+
+ /**
+ * Returns true if the given {@link CanvasViewInfo} represents an element we
+ * should remove in a flattening conversion. We don't want to remove non-layout
+ * views, or layout views that for example contain drawables on their own.
+ */
+ private boolean isRemovableLayout(CanvasViewInfo child) {
+ // The element being converted is NOT removable!
+ Element element = getElement(child);
+ if (element == mLayout) {
+ return false;
+ }
+
+ ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
+ String name = descriptor.getXmlLocalName();
+ if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) {
+ // Don't delete layouts that provide a background image or gradient
+ if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Did not flatten layout %1$s because it defines a '%2$s' attribute",
+ VisualRefactoring.getId(element), ATTR_BACKGROUND);
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
index cae49ca..e496b7a 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
@@ -27,17 +27,20 @@ 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.annotations.VisibleForTesting;
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.descriptors.ViewElementDescriptor;
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.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.util.Pair;
import org.eclipse.core.resources.IFile;
@@ -50,6 +53,7 @@ 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.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;
@@ -62,12 +66,14 @@ 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.MalformedTreeException;
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.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;
@@ -95,19 +101,23 @@ import java.util.Set;
*/
@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;
+ private static final String KEY_FILE = "file"; //$NON-NLS-1$
+ private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$
+ private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
+ private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
+
+ protected final IFile mFile;
+ protected final LayoutEditor mEditor;
+ protected final IProject mProject;
protected int mSelectionStart = -1;
protected int mSelectionEnd = -1;
- protected List<Element> mElements = null;
- protected ITreeSelection mTreeSelection;
- protected ITextSelection mSelection;
+ protected final List<Element> mElements;
+ protected final ITreeSelection mTreeSelection;
+ protected final ITextSelection mSelection;
+
+ protected final Map<Element, String> mGeneratedIdMap = new HashMap<Element, String>();
+ protected final Set<String> mGeneratedIds = new HashSet<String>();
+
protected List<Change> mChanges;
private String mAndroidNamespacePrefix;
@@ -124,6 +134,36 @@ public abstract class VisualRefactoring extends Refactoring {
mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
mEditor = null;
+ mElements = null;
+ mSelection = null;
+ mTreeSelection = null;
+ }
+
+ @VisibleForTesting
+ VisualRefactoring(List<Element> elements, LayoutEditor editor) {
+ mElements = elements;
+ mEditor = editor;
+
+ mFile = editor != null ? editor.getInputFile() : null;
+ mProject = editor != null ? editor.getProject() : null;
+ mSelectionStart = 0;
+ mSelectionEnd = 0;
+ mSelection = null;
+ mTreeSelection = null;
+
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (Element element : elements) {
+ if (element instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) element;
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ }
}
public VisualRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
@@ -166,6 +206,8 @@ public abstract class VisualRefactoring extends Refactoring {
mSelectionStart = selection.getOffset();
mSelectionEnd = mSelectionStart + selection.getLength();
}
+
+ mElements = initElements();
}
@Override
@@ -267,8 +309,7 @@ public abstract class VisualRefactoring extends Refactoring {
}
}
- // This also ensures that we have a valid DOM model:
- mElements = getElements();
+ // Ensures that we have a valid DOM model:
if (mElements.size() == 0) {
status.addFatalError("Nothing to extract");
return status;
@@ -291,6 +332,11 @@ public abstract class VisualRefactoring extends Refactoring {
mChanges = new ArrayList<Change>();
try {
monitor.beginTask("Checking post-conditions...", 5);
+
+ // Reset state for each computeChanges call, in case the user goes back
+ // and forth in the refactoring wizard
+ mGeneratedIdMap.clear();
+ mGeneratedIds.clear();
List<Change> changes = computeChanges();
mChanges.addAll(changes);
@@ -661,17 +707,60 @@ public abstract class VisualRefactoring extends Refactoring {
}
protected List<Element> getElements() {
- if (mElements == null) {
- List<Element> nodes = new ArrayList<Element>();
+ return mElements;
+ }
+
+ protected List<Element> initElements() {
+ List<Element> nodes = new ArrayList<Element>();
+
+ assert mTreeSelection == null || mSelection == null :
+ "treeSel= " + mTreeSelection + ", sel=" + mSelection;
+
+ // 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 (mTreeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : mTreeSelection.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 Element) {
+ Element element = (Element) xmlNode;
+ nodes.add(element);
+ IndexedRegion region = getRegion(element);
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ }
+ } else if (mSelection != null) {
+ mSelectionStart = mSelection.getOffset();
+ mSelectionEnd = mSelectionStart + mSelection.getLength();
- AndroidXmlEditor editor = mEditor;
- IStructuredDocument doc = editor.getStructuredDocument();
+ // Figure out the range of selected nodes from the document offsets
+ IStructuredDocument doc = mEditor.getStructuredDocument();
Pair<Element, Element> range = DomUtilities.getElementRange(doc,
mSelectionStart, mSelectionEnd);
if (range != null) {
Element first = range.getFirst();
Element last = range.getSecond();
+ // Adjust offsets to get rid of surrounding text nodes (if you happened
+ // to select a text range and included whitespace on either end etc)
+ mSelectionStart = getRegion(first).getStartOffset();
+ mSelectionEnd = getRegion(last).getEndOffset();
+
if (first == last) {
nodes.add(first);
} else if (first.getParentNode() == last.getParentNode()) {
@@ -691,10 +780,18 @@ public abstract class VisualRefactoring extends Refactoring {
// elements from different levels. We can't extract ranges like that.
}
}
- mElements = nodes;
+ } else {
+ assert false;
}
- return mElements;
+ // Make sure that the list of elements is unique
+ //Set<Element> seen = new HashSet<Element>();
+ //for (Element element : nodes) {
+ // assert !seen.contains(element) : element;
+ // seen.add(element);
+ //}
+
+ return nodes;
}
protected Element getPrimaryElement() {
@@ -707,7 +804,11 @@ public abstract class VisualRefactoring extends Refactoring {
}
protected Document getDomDocument() {
- return mEditor.getUiRootNode().getXmlDocument();
+ if (mEditor.getUiRootNode() != null) {
+ return mEditor.getUiRootNode().getXmlDocument();
+ } else {
+ return getElements().get(0).getOwnerDocument();
+ }
}
protected List<CanvasViewInfo> getSelectedViewInfos() {
@@ -793,7 +894,20 @@ public abstract class VisualRefactoring extends Refactoring {
return true;
}
- protected IndexedRegion getRegion(Node node) {
+ protected void ensureIdMatchesType(Element element, String newType, MultiTextEdit rootEdit) {
+ String oldType = element.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ String oldTypeBase = oldType.substring(oldType.lastIndexOf('.') + 1);
+ String id = getId(element);
+ if (id == null || id.toLowerCase().contains(oldTypeBase.toLowerCase())) {
+ String newTypeBase = newType.substring(newType.lastIndexOf('.') + 1);
+ ensureHasId(rootEdit, element, newTypeBase);
+ }
+ }
+
+ protected static IndexedRegion getRegion(Node node) {
if (node instanceof IndexedRegion) {
return (IndexedRegion) node;
}
@@ -801,11 +915,21 @@ public abstract class VisualRefactoring extends Refactoring {
return null;
}
- protected String ensureHasId(MultiTextEdit rootEdit, Element element) {
- if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
- String id = DomUtilities.getFreeWidgetId(element);
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element, String prefix) {
+ String id = mGeneratedIdMap.get(element);
+ if (id != null) {
+ return NEW_ID_PREFIX + id;
+ }
+
+ if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)
+ || (prefix != null && !getId(element).startsWith(prefix))) {
+ id = DomUtilities.getFreeWidgetId(element, mGeneratedIds, prefix);
+ // Make sure we don't use this one again
+ mGeneratedIds.add(id);
+ mGeneratedIdMap.put(element, id);
id = NEW_ID_PREFIX + id;
- addAttributeDeclaration(rootEdit, element, getAndroidNamespacePrefix(), ATTR_ID, id);
+ setAttribute(rootEdit, element,
+ ANDROID_URI, getAndroidNamespacePrefix(), ATTR_ID, id);
return id;
}
@@ -828,7 +952,7 @@ public abstract class VisualRefactoring extends Refactoring {
return -1;
}
- protected static String getId(Element element) {
+ public static String getId(Element element) {
return element.getAttributeNS(ANDROID_URI, ATTR_ID);
}
@@ -855,16 +979,22 @@ public abstract class VisualRefactoring extends Refactoring {
return fqcn;
}
- protected void addAttributeDeclaration(MultiTextEdit rootEdit, Element element,
+ protected void setAttribute(MultiTextEdit rootEdit, Element element,
+ String attributeUri,
String attributePrefix, String attributeName, String attributeValue) {
int offset = getFirstAttributeOffset(element);
if (offset != -1) {
- addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
- attributeValue);
+ if (element.hasAttributeNS(attributeUri, attributeName)) {
+ replaceAttributeDeclaration(rootEdit, offset, element, attributePrefix,
+ attributeUri, attributeName, attributeValue);
+ } else {
+ addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
+ attributeValue);
+ }
}
}
- protected void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ private void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
String attributePrefix, String attributeName, String attributeValue) {
StringBuilder sb = new StringBuilder();
sb.append(' ').append(attributePrefix).append(':');
@@ -875,6 +1005,58 @@ public abstract class VisualRefactoring extends Refactoring {
rootEdit.addChild(setAttribute);
}
+ /** Replaces the value declaration of the given attribute */
+ private void replaceAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ Element element, String attributePrefix, String attributeUri,
+ String attributeName, String attributeValue) {
+ // Find attribute value and replace it
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ int valueStart = -1;
+ boolean useNextValue = false;
+ String targetName = attributePrefix + ':' + attributeName;
+
+ // Look at all attribute values and look for an id reference match
+ 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)) {
+ // What about prefix?
+ if (targetName.equals(region.getText(subRegion))) {
+ useNextValue = true;
+ }
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ if (useNextValue) {
+ valueStart = regionStart + subRegion.getStart();
+ break;
+ }
+ }
+ }
+
+ if (valueStart != -1) {
+ String oldValue = element.getAttributeNS(attributeUri, attributeName);
+ int start = valueStart + 1; // Skip opening "
+ ReplaceEdit setAttribute = new ReplaceEdit(start, oldValue.length(),
+ attributeValue);
+ try {
+ rootEdit.addChild(setAttribute);
+ } catch (MalformedTreeException mte) {
+ AdtPlugin.log(mte, "Could not replace attribute %1$s with %2$s",
+ attributeName, attributeValue);
+ throw mte;
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
/** Strips out the given attribute, if defined */
protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri,
String attributeName) {
@@ -890,6 +1072,230 @@ public abstract class VisualRefactoring extends Refactoring {
}
}
+ /**
+ * Removes the given element's opening and closing tags (including all of its
+ * attributes) but leaves any children alone
+ *
+ * @param rootEdit the multi edit to add the removal operation to
+ * @param element the element to delete the open and closing tags for
+ * @param skip a list of elements that should not be modified (for example because they
+ * are targeted for deletion)
+ *
+ * TODO: Rename this to "unwrap" ? And allow for handling nested deletions.
+ */
+ protected void removeElementTags(MultiTextEdit rootEdit, Element element, List<Element> skip) {
+ IndexedRegion elementRegion = getRegion(element);
+ if (elementRegion == null) {
+ return;
+ }
+
+ // Look for the opening tag
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ int startLineInclusive = -1;
+ int endLineInclusive = -1;
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ int start = elementRegion.getStartOffset();
+ IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+ int startOffset = regionStart;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ startLineInclusive = doc.getLineOfOffset(endOffset) + 1;
+ break;
+ }
+ }
+
+
+
+ // Find the close tag
+ // Look at all attribute values and look for an id reference match
+ region = doc.getRegionAtCharacterOffset(elementRegion.getEndOffset()
+ - element.getTagName().length() - 1);
+ list = region.getRegions();
+ regionStart = region.getStartOffset();
+ startOffset = -1;
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
+ startOffset = regionStart + subRegion.getStart();
+ } else if (DOMRegionContext.XML_TAG_CLOSE.equals(type)) {
+ int endOffset = regionStart + subRegion.getStart() + subRegion.getLength();
+ if (startOffset != -1) {
+ DeleteEdit deletion = createDeletion(doc, startOffset, endOffset);
+ rootEdit.addChild(deletion);
+ endLineInclusive = doc.getLineOfOffset(startOffset) - 1;
+ }
+ break;
+ }
+ }
+ }
+
+ // Dedent the contents
+ if (startLineInclusive != -1 && endLineInclusive != -1) {
+ String indent = AndroidXmlEditor.getIndentAtOffset(doc, getRegion(element)
+ .getStartOffset());
+ setIndentation(rootEdit, indent, doc, startLineInclusive, endLineInclusive,
+ element, skip);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ protected void removeIndentation(MultiTextEdit rootEdit, String removeIndent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = removeIndent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart,
+ lineStart + Math.min(lineLength, indentLength));
+ if (lineText.startsWith(removeIndent)) {
+ rootEdit.addChild(new DeleteEdit(lineStart, indentLength));
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ protected void setIndentation(MultiTextEdit rootEdit, String indent,
+ IStructuredDocument doc, int startLineInclusive, int endLineInclusive,
+ Element element, List<Element> skip) {
+ if (startLineInclusive > endLineInclusive) {
+ return;
+ }
+ int indentLength = indent.length();
+ if (indentLength == 0) {
+ return;
+ }
+
+ try {
+ for (int line = startLineInclusive; line <= endLineInclusive; line++) {
+ IRegion info = doc.getLineInformation(line);
+ int lineStart = info.getOffset();
+ int lineLength = info.getLength();
+ int lineEnd = lineStart + lineLength;
+ if (overlaps(lineStart, lineEnd, element, skip)) {
+ continue;
+ }
+ String lineText = getText(lineStart, lineStart + lineLength);
+ int indentEnd = getFirstNonSpace(lineText);
+ rootEdit.addChild(new ReplaceEdit(lineStart, indentEnd, indent));
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private int getFirstNonSpace(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return i;
+ }
+ }
+
+ return s.length();
+ }
+
+ /** Returns true if the given line overlaps any of the given elements */
+ private static boolean overlaps(int startOffset, int endOffset,
+ Element element, List<Element> overlaps) {
+ for (Element e : overlaps) {
+ if (e == element) {
+ continue;
+ }
+
+ IndexedRegion region = getRegion(e);
+ if (region.getEndOffset() >= startOffset && region.getStartOffset() <= endOffset) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected DeleteEdit createDeletion(IStructuredDocument doc, int startOffset, int endOffset) {
+ // Expand to delete the whole line?
+ try {
+ IRegion info = doc.getLineInformationOfOffset(startOffset);
+ int lineBegin = info.getOffset();
+ // Is the text on the line leading up to the deletion region,
+ // and the text following it, all whitespace?
+ boolean deleteLine = true;
+ if (lineBegin < startOffset) {
+ String prefix = getText(lineBegin, startOffset);
+ if (prefix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ info = doc.getLineInformationOfOffset(endOffset);
+ int lineEnd = info.getOffset() + info.getLength();
+ if (lineEnd > endOffset) {
+ String suffix = getText(endOffset, lineEnd);
+ if (suffix.trim().length() > 0) {
+ deleteLine = false;
+ }
+ }
+ if (deleteLine) {
+ startOffset = lineBegin;
+ endOffset = lineEnd + 1;
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+
+ return new DeleteEdit(startOffset, endOffset - startOffset);
+ }
+
+ protected ViewElementDescriptor getElementDescriptor(String fqcn) {
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ List<ViewElementDescriptor> views =
+ data.getLayoutDescriptors().getViewDescriptors();
+ for (ViewElementDescriptor descriptor : views) {
+ if (fqcn.equals(descriptor.getFullClassName())) {
+ return descriptor;
+ }
+ }
+ List<ViewElementDescriptor> layouts =
+ data.getLayoutDescriptors().getLayoutDescriptors();
+ for (ViewElementDescriptor descriptor : layouts) {
+ if (fqcn.equals(descriptor.getFullClassName())) {
+ return descriptor;
+ }
+ }
+ }
+
+ return null;
+ }
+
public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor {
private final Map<String, String> mArguments;
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java
new file mode 100644
index 0000000..dceba6e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringWizard.java
@@ -0,0 +1,41 @@
+/*
+ * 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.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+
+public abstract class VisualRefactoringWizard extends RefactoringWizard {
+ protected final LayoutEditor mEditor;
+
+ public VisualRefactoringWizard(Refactoring refactoring, LayoutEditor editor) {
+ super(refactoring, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mEditor = editor;
+ }
+
+ @Override
+ public boolean performFinish() {
+ mEditor.setIgnoreXmlUpdate(true);
+ try {
+ return super.performFinish();
+ } finally {
+ mEditor.setIgnoreXmlUpdate(false);
+ mEditor.refreshXmlModel();
+ }
+ }
+}
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
index 865e406..1fd1837 100644
--- 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
@@ -31,7 +31,7 @@ public class WrapInAction extends VisualRefactoringAction {
if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
WrapInRefactoring ref = new WrapInRefactoring(mFile, mEditor,
mTextSelection, mTreeSelection);
- RefactoringWizard wizard = new WrapInWizard(ref, mFile.getProject());
+ RefactoringWizard wizard = new WrapInWizard(ref, mEditor);
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
index 4a73361..3c95d94 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
@@ -16,6 +16,7 @@
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_ID;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
@@ -24,6 +25,7 @@ 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.AdtConstants.EXT_XML;
+import com.android.annotations.VisibleForTesting;
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;
@@ -84,6 +86,11 @@ public class WrapInRefactoring extends VisualRefactoring {
super(file, editor, selection, treeSelection);
}
+ @VisibleForTesting
+ WrapInRefactoring(List<Element> selectedElements, LayoutEditor editor) {
+ super(selectedElements, editor);
+ }
+
@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
@@ -113,8 +120,7 @@ public class WrapInRefactoring extends VisualRefactoring {
}
}
- // This also ensures that we have a valid DOM model:
- mElements = getElements();
+ // Ensures that we have a valid DOM model:
if (mElements.size() == 0) {
status.addFatalError("Nothing to wrap");
return status;
@@ -372,6 +378,19 @@ public class WrapInRefactoring extends VisualRefactoring {
return changes;
}
+ 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;
+ }
+
public static class Descriptor extends VisualRefactoringDescriptor {
public Descriptor(String project, String description, String comment,
Map<String, String> arguments) {
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
index 8119392..caee8f7 100644
--- 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
@@ -16,8 +16,17 @@
package com.android.ide.eclipse.adt.internal.editors.layout.refactoring;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_GESTURE_OVERLAY_VIEW;
import static com.android.ide.common.layout.LayoutConstants.FQCN_LINEAR_LAYOUT;
-
+import static com.android.ide.common.layout.LayoutConstants.FQCN_RADIO_BUTTON;
+import static com.android.ide.common.layout.LayoutConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.ide.common.layout.LayoutConstants.RADIO_GROUP;
+import static com.android.sdklib.SdkConstants.CLASS_VIEW;
+import static com.android.sdklib.SdkConstants.CLASS_VIEWGROUP;
+import static com.android.sdklib.SdkConstants.FN_FRAMEWORK_LIBRARY;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+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.editors.layout.gre.ViewMetadataRepository;
import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
@@ -28,7 +37,22 @@ 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.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jdt.internal.core.ResolvedBinaryType;
+import org.eclipse.jdt.internal.core.ResolvedSourceType;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
@@ -43,32 +67,39 @@ import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
+import java.util.ArrayList;
import java.util.List;
-class WrapInWizard extends RefactoringWizard {
- private final IProject mProject;
+@SuppressWarnings("restriction") // JDT model access for custom-view class lookup
+class WrapInWizard extends VisualRefactoringWizard {
+ private static final String SEPARATOR_LABEL =
+ "----------------------------------------"; //$NON-NLS-1$
- public WrapInWizard(WrapInRefactoring ref, IProject project) {
- super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
- mProject = project;
+ public WrapInWizard(WrapInRefactoring ref, LayoutEditor editor) {
+ super(ref, editor);
setDefaultPageTitle("Wrap in Container");
}
@Override
protected void addUserInputPages() {
- addPage(new InputPage(mProject));
+ WrapInRefactoring ref = (WrapInRefactoring) getRefactoring();
+ String oldType = ref.getOldType();
+ addPage(new InputPage(mEditor.getProject(), oldType));
}
/** Wizard page which inputs parameters for the {@link WrapInRefactoring} operation */
private static class InputPage extends UserInputWizardPage {
private final IProject mProject;
+ private final String mOldType;
private Text mIdText;
private Combo mTypeCombo;
private Button mUpdateReferences;
+ private List<String> mClassNames;
- public InputPage(IProject project) {
+ public InputPage(IProject project, String oldType) {
super("WrapInInputPage"); //$NON-NLS-1$
mProject = project;
+ mOldType = oldType;
}
public void createControl(Composite parent) {
@@ -108,7 +139,8 @@ class WrapInWizard extends RefactoringWizard {
mUpdateReferences.setText("Update layout references");
mUpdateReferences.addSelectionListener(selectionListener);
- addLayouts(mProject, mTypeCombo, null);
+ mClassNames = addLayouts(mProject, mOldType, mTypeCombo, null, true);
+ mTypeCombo.select(0);
setControl(composite);
validatePage();
@@ -139,9 +171,11 @@ class WrapInWizard extends RefactoringWizard {
}
}
- if (mTypeCombo.getText().equals(SEPARATOR_LABEL)) {
+ int selectionIndex = mTypeCombo.getSelectionIndex();
+ String type = selectionIndex != -1 ? mClassNames.get(selectionIndex) : null;
+ if (type == null) {
setErrorMessage("Select a container type");
- ok = false;
+ ok = false; // The user has chosen a separator
}
if (ok) {
@@ -151,7 +185,7 @@ class WrapInWizard extends RefactoringWizard {
WrapInRefactoring refactoring =
(WrapInRefactoring) getRefactoring();
refactoring.setId(id);
- refactoring.setType(mTypeCombo.getText());
+ refactoring.setType(type);
refactoring.setUpdateReferences(mUpdateReferences.getSelection());
}
@@ -160,18 +194,34 @@ class WrapInWizard extends RefactoringWizard {
}
}
- static final String SEPARATOR_LABEL =
- "----------------------------------------"; //$NON-NLS-1$
+ static List<String> addLayouts(IProject project, String oldType, Combo combo, String exclude,
+ boolean addGestureOverlay) {
+ List<String> classNames = new ArrayList<String>();
+
+ if (oldType.equals(FQCN_RADIO_BUTTON)) {
+ combo.add(RADIO_GROUP);
+ // NOT a fully qualified name since android widgets do not include the package
+ classNames.add(RADIO_GROUP);
+
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ Pair<List<String>,List<String>> result = findViews(project, true);
+ List<String> customViews = result.getFirst();
+ List<String> thirdPartyViews = result.getSecond();
+ if (customViews.size() > 0) {
+ for (String view : customViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
- 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);
@@ -196,12 +246,31 @@ class WrapInWizard extends RefactoringWizard {
for (ViewElementDescriptor d : layoutDescriptors) {
String className = d.getFullClassName();
if (exclude == null || !exclude.equals(className)) {
- combo.add(className);
+ combo.add(d.getUiName());
+ classNames.add(className);
}
}
// SWT does not support separators in combo boxes
combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+
+ if (thirdPartyViews.size() > 0) {
+ for (String view : thirdPartyViews) {
+ combo.add(view);
+ classNames.add(view);
+ }
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
+
+ if (addGestureOverlay) {
+ combo.add(GESTURE_OVERLAY_VIEW);
+ classNames.add(FQCN_GESTURE_OVERLAY_VIEW);
+
+ combo.add(SEPARATOR_LABEL);
+ classNames.add(null);
+ }
}
// Now add ALL known layout descriptors in case the user has
@@ -212,15 +281,79 @@ class WrapInWizard extends RefactoringWizard {
for (ViewElementDescriptor d : layoutDescriptors) {
String className = d.getFullClassName();
if (exclude == null || !exclude.equals(className)) {
- combo.add(className);
+ combo.add(d.getUiName());
+ classNames.add(className);
}
}
}
}
- combo.select(initialIndex);
} else {
combo.add("SDK not initialized");
- combo.setEnabled(false);
+ classNames.add(null);
+ }
+
+ return classNames;
+ }
+
+ /**
+ * Returns a pair of view lists - the custom views and the 3rd-party views
+ *
+ * @param project the Android project
+ * @param layoutsOnly if true, only search for layouts
+ * @return a pair of lists, the first containing custom views and the second
+ * containing 3rd party views
+ */
+ public static Pair<List<String>,List<String>> findViews(IProject project, boolean layoutsOnly) {
+ final List<String> customViews = new ArrayList<String>();
+ final List<String> thirdPartyViews = new ArrayList<String>();
+
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ Object element = match.getElement();
+
+ if (element instanceof ResolvedBinaryType) {
+ ResolvedBinaryType bt = (ResolvedBinaryType) element;
+ IPackageFragment fragment = bt.getPackageFragment();
+ IPath path = fragment.getPath();
+ String last = path.lastSegment();
+ // Filter out android.jar stuff
+ if (last.equals(FN_FRAMEWORK_LIBRARY)) {
+ return;
+ }
+ String fqn = bt.getFullyQualifiedName();
+ thirdPartyViews.add(fqn);
+ } else if (element instanceof ResolvedSourceType) {
+ ResolvedSourceType type = (ResolvedSourceType) element;
+ String fqn = type.getFullyQualifiedName();
+ // User custom view
+ customViews.add(fqn);
+ }
+ }
+ };
+ try {
+ IJavaProject javaProject = (IJavaProject) project.getNature(JavaCore.NATURE_ID);
+ if (javaProject != null) {
+ String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
+ IType activityType = javaProject.findType(className);
+ if (activityType != null) {
+ IJavaSearchScope scope = SearchEngine.createHierarchyScope(activityType);
+ SearchParticipant[] participants = new SearchParticipant[] {
+ SearchEngine.getDefaultSearchParticipant()
+ };
+ int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
+ SearchPattern pattern = SearchPattern.createPattern("*",
+ IJavaSearchConstants.CLASS, IJavaSearchConstants.DECLARATIONS,
+ matchRule);
+ SearchEngine engine = new SearchEngine();
+ engine.search(pattern, participants, scope, requestor,
+ new NullProgressMonitor());
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
}
+
+ return Pair.of(customViews, thirdPartyViews);
}
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
index 596cfe3..9c822f6 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.internal.editors.uimodel;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import org.eclipse.swt.widgets.Composite;
@@ -38,8 +39,8 @@ public abstract class UiAttributeNode {
private boolean mIsDirty;
private boolean mHasError;
- /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor}
- * and the corresponding runtine {@link UiElementNode} parent. */
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor}
+ * and the corresponding runtime {@link UiElementNode} parent. */
public UiAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
mDescriptor = attributeDescriptor;
mUiParent = uiParent;
@@ -54,7 +55,7 @@ public abstract class UiAttributeNode {
public final UiElementNode getUiParent() {
return mUiParent;
}
-
+
/** Returns the current value of the node. */
public abstract String getCurrentValue();
@@ -78,10 +79,13 @@ public abstract class UiAttributeNode {
mIsDirty = isDirty;
// TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null
if (old_value != isDirty) {
- getUiParent().getEditor().editorDirtyStateChanged();
+ AndroidXmlEditor editor = getUiParent().getEditor();
+ if (editor != null) {
+ editor.editorDirtyStateChanged();
+ }
}
}
-
+
/**
* Sets the error flag value.
* @param errorFlag the error flag
@@ -89,21 +93,21 @@ public abstract class UiAttributeNode {
public final void setHasError(boolean errorFlag) {
mHasError = errorFlag;
}
-
+
/**
* Returns whether this node has errors.
*/
public final boolean hasError() {
return mHasError;
}
-
+
/**
* Called once by the parent user interface to creates the necessary
* user interface to edit this attribute.
* <p/>
* This method can be called more than once in the life cycle of an UI node,
* typically when the UI is part of a master-detail tree, as pages are swapped.
- *
+ *
* @param parent The composite where to create the user interface.
* @param managedForm The managed form owning this part.
*/
@@ -116,7 +120,7 @@ public abstract class UiAttributeNode {
* for an attribute.
* <p/>
* Implementations that do not have any known values should return null.
- *
+ *
* @param prefix An optional prefix string, which is whatever the user has already started
* typing. Can be null or an empty string. The implementation can use this to filter choices
* and only return strings that match this prefix. A lazy or default implementation can
@@ -136,7 +140,7 @@ public abstract class UiAttributeNode {
* The caller doesn't really know if attributes have changed,
* so it will call this to refresh the attribute anyway. It's up to the
* UI implementation to minimize refreshes.
- *
+ *
* @param xml_attribute_node
*/
public abstract void updateValue(Node xml_attribute_node);
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
index 36cfc80..01d4e4e 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
@@ -23,6 +23,7 @@ import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttr
import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_URI;
import static com.android.sdklib.SdkConstants.NS_RESOURCES;
+import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
@@ -1436,8 +1437,8 @@ public class UiElementNode implements IPropertySource {
// --- for derived implementations only ---
- // TODO doc
- protected void setXmlNode(Node xmlNode) {
+ @VisibleForTesting
+ public void setXmlNode(Node xmlNode) {
mXmlNode = xmlNode;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java
index b02d1a8..51dccf1 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java
@@ -434,7 +434,7 @@ public class NewProjectWizard extends Wizard implements INewWizard {
IPath path = info.getLocationPath();
IPath defaultLocation = Platform.getLocation();
- if (!path.equals(defaultLocation)) {
+ if (path != null && !path.equals(defaultLocation)) {
description.setLocation(path);
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java
index 8192361..fb33a08 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java
@@ -69,7 +69,13 @@ public class WorkingSetGroup {
}
public IWorkingSet[] getSelectedWorkingSets() {
- return fWorkingSetBlock.getSelectedWorkingSets();
+ try {
+ return fWorkingSetBlock.getSelectedWorkingSets();
+ } catch (Throwable t) {
+ // Test scenarios; no UI is created, which the fWorkingSetBlock assumes
+ // (it dereferences the enabledButton)
+ return new IWorkingSet[0];
+ }
}
public boolean isChecked() {
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java
new file mode 100644
index 0000000..a8e1f26
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java
@@ -0,0 +1,405 @@
+/*
+ * 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.AndroidConstants.FD_RES_LAYOUT;
+import static com.android.sdklib.SdkConstants.FD_RES;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+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.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreationPage;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizard;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectCreationPage;
+import com.android.ide.eclipse.tests.SdkTestCase;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IWorkingSet;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+
+@SuppressWarnings("restriction")
+public class AdtProjectTest extends SdkTestCase {
+ /**
+ * Individual tests don't share an instance of the TestCase so we stash the test
+ * project in a static field such that we don't need to keep recreating it -- should
+ * be much faster.
+ */
+ protected static IProject sProject;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ if (sProject == null) {
+ IProject project = null;
+ String projectName = "testproject-" + System.currentTimeMillis();
+ project = createProject(projectName);
+ assertNotNull(project);
+ sProject = project;
+ }
+ }
+
+ protected IProject getProject() {
+ return sProject;
+ }
+
+ protected IFile getTestFile(IProject project, String name) throws Exception {
+ IFolder resFolder = project.getFolder(FD_RES);
+ NullProgressMonitor monitor = new NullProgressMonitor();
+ if (!resFolder.exists()) {
+ resFolder.create(true /* force */, true /* local */, monitor);
+ }
+ IFolder layoutfolder = resFolder.getFolder(FD_RES_LAYOUT);
+ if (!layoutfolder.exists()) {
+ layoutfolder.create(true /* force */, true /* local */, monitor);
+ }
+ IFile file = layoutfolder.getFile(name);
+ if (!file.exists()) {
+ String xml = readTestFile(name, true);
+ InputStream bstream = new ByteArrayInputStream(xml.getBytes("UTF-8")); //$NON-NLS-1$
+ file.create(bstream, false /* force */, monitor);
+ }
+
+ return file;
+ }
+
+ protected IProject createProject(String name) {
+ IAndroidTarget target = null;
+
+ IAndroidTarget[] targets = getSdk().getTargets();
+ for (IAndroidTarget t : targets) {
+ if (t.getVersion().getApiLevel() >= 11) {
+ target = t;
+ break;
+ }
+ }
+ assertNotNull(target);
+
+ final StubProjectWizard newProjCreator = new StubProjectWizard(
+ name, target);
+ newProjCreator.init(null, null);
+ // need to run finish on ui thread since it invokes a perspective switch
+ Display.getDefault().syncExec(new Runnable() {
+ public void run() {
+ newProjCreator.performFinish();
+ }
+ });
+
+ return validateProjectExists(name);
+ }
+
+ public void createTestProject() {
+ IAndroidTarget target = null;
+
+ IAndroidTarget[] targets = getSdk().getTargets();
+ for (IAndroidTarget t : targets) {
+ if (t.getVersion().getApiLevel() >= 11) {
+ target = t;
+ break;
+ }
+ }
+ assertNotNull(target);
+ }
+
+ private static IProject validateProjectExists(String name) {
+ IProject iproject = getProject(name);
+ assertTrue(String.format("%s project not created", name), iproject.exists());
+ assertTrue(String.format("%s project not opened", name), iproject.isOpen());
+ return iproject;
+ }
+
+ private static IProject getProject(String name) {
+ IProject iproject = ResourcesPlugin.getWorkspace().getRoot()
+ .getProject(name);
+ return iproject;
+ }
+
+ public static ViewElementDescriptor createDesc(String name, String fqn, boolean hasChildren) {
+ if (hasChildren) {
+ return new ViewElementDescriptor(name, name, fqn, "", "", new AttributeDescriptor[0],
+ new AttributeDescriptor[0], new ElementDescriptor[1], false);
+ } else {
+ return new ViewElementDescriptor(name, fqn);
+ }
+ }
+
+ public static UiViewElementNode createNode(UiViewElementNode parent, String fqn,
+ boolean hasChildren) {
+ String name = fqn.substring(fqn.lastIndexOf('.') + 1);
+ ViewElementDescriptor descriptor = createDesc(name, fqn, hasChildren);
+ if (parent == null) {
+ // All node hierarchies should be wrapped inside a document node at the root
+ parent = new UiViewElementNode(createDesc("doc", "doc", true));
+ }
+ return (UiViewElementNode) parent.appendNewUiChild(descriptor);
+ }
+
+ public static UiViewElementNode createNode(String fqn, boolean hasChildren) {
+ return createNode(null, fqn, hasChildren);
+ }
+
+ protected String readTestFile(String relativePath, boolean expectExists) {
+ String path = "testdata" + File.separator + relativePath; //$NON-NLS-1$
+ InputStream stream =
+ AdtProjectTest.class.getResourceAsStream(path);
+ if (!expectExists && stream == null) {
+ return null;
+ }
+
+ assertNotNull(stream);
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+ String xml = AdtPlugin.readFile(reader);
+ assertNotNull(xml);
+ assertTrue(xml.length() > 0);
+
+ return xml;
+ }
+
+ /** Special editor context set on the model to be rendered */
+ protected static class TestLayoutEditor extends LayoutEditor {
+ private final IFile mFile;
+ private final IStructuredDocument mStructuredDocument;
+ private UiDocumentNode mUiRootNode;
+
+ public TestLayoutEditor(IFile file, IStructuredDocument structuredDocument,
+ UiDocumentNode uiRootNode) {
+ mFile = file;
+ mStructuredDocument = structuredDocument;
+ mUiRootNode = uiRootNode;
+ }
+
+ @Override
+ public IFile getInputFile() {
+ return mFile;
+ }
+
+ @Override
+ public IProject getProject() {
+ return mFile.getProject();
+ }
+
+ @Override
+ public IStructuredDocument getStructuredDocument() {
+ return mStructuredDocument;
+ }
+
+ @Override
+ public UiDocumentNode getUiRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ public void editorDirtyStateChanged() {
+ }
+
+ @Override
+ public IStructuredModel getModelForRead() {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ try {
+ return mm.getModelForRead(mFile);
+ } catch (Exception e) {
+ fail(e.toString());
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * Stub class for project creation wizard.
+ * <p/>
+ * Created so project creation logic can be run without UI creation/manipulation.
+ */
+ public class StubProjectWizard extends NewProjectWizard {
+
+ private final String mProjectName;
+ private final IAndroidTarget mTarget;
+
+ public StubProjectWizard(String projectName, IAndroidTarget target) {
+ this.mProjectName = projectName;
+ this.mTarget = target;
+ }
+
+ /**
+ * Override parent to return stub page
+ */
+ @Override
+ protected NewProjectCreationPage createMainPage() {
+ return new StubProjectCreationPage(mProjectName, mTarget);
+ }
+
+ /**
+ * Override parent to return null page
+ */
+ @Override
+ protected NewTestProjectCreationPage createTestPage() {
+ return null;
+ }
+
+ /**
+ * Overrides parent to return dummy wizard container
+ */
+ @Override
+ public IWizardContainer getContainer() {
+ return new IWizardContainer() {
+
+ public IWizardPage getCurrentPage() {
+ return null;
+ }
+
+ public Shell getShell() {
+ return null;
+ }
+
+ public void showPage(IWizardPage page) {
+ // pass
+ }
+
+ public void updateButtons() {
+ // pass
+ }
+
+ public void updateMessage() {
+ // pass
+ }
+
+ public void updateTitleBar() {
+ // pass
+ }
+
+ public void updateWindowTitle() {
+ // pass
+ }
+
+ /**
+ * Executes runnable on current thread
+ */
+ public void run(boolean fork, boolean cancelable,
+ IRunnableWithProgress runnable)
+ throws InvocationTargetException, InterruptedException {
+ runnable.run(new NullProgressMonitor());
+ }
+
+ };
+ }
+ }
+
+ /**
+ * Stub class for project creation page.
+ * <p/>
+ * Returns canned responses for creating a sample project.
+ */
+ public class StubProjectCreationPage extends NewProjectCreationPage {
+
+ private final String mProjectName;
+ private final IAndroidTarget mTarget;
+
+ public StubProjectCreationPage(String projectName, IAndroidTarget target) {
+ super();
+ this.mProjectName = projectName;
+ this.mTarget = target;
+ setTestInfo(null);
+ }
+
+ @Override
+ public IMainInfo getMainInfo() {
+ return new IMainInfo() {
+ public String getProjectName() {
+ return mProjectName;
+ }
+
+ public String getPackageName() {
+ return "com.android.eclipse.tests";
+ }
+
+ public String getActivityName() {
+ return mProjectName;
+ }
+
+ public String getApplicationName() {
+ return mProjectName;
+ }
+
+ public boolean isNewProject() {
+ return true;
+ }
+
+ public String getSourceFolder() {
+ return "src";
+ }
+
+ public IPath getLocationPath() {
+ // Default location
+ return null;//new Path(mLocation);
+ }
+
+ public String getMinSdkVersion() {
+ return null;
+ }
+
+ public IAndroidTarget getSdkTarget() {
+ return mTarget;
+ }
+
+ public boolean isCreateActivity() {
+ return false;
+ }
+
+ public boolean useDefaultLocation() {
+ return true;
+ }
+
+ public IWorkingSet[] getSelectedWorkingSets() {
+ return new IWorkingSet[0];
+ }
+ };
+ }
+ }
+
+ public void testDummy() {
+ // This class contains shared test functionality for testcase subclasses,
+ // but without an actual test in the class JUnit complains (even if we make
+ // it abstract)
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java
new file mode 100644
index 0000000..956fc5d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoringTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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_RELATIVE_LAYOUT;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.w3c.dom.Element;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeLayoutRefactoringTest extends RefactoringTest {
+
+ public void testChangeLayout1a() throws Exception {
+ // Test a basic layout which performs some nesting -- tests basic grid layout conversion
+ checkRefactoring("sample1a.xml", true);
+ }
+
+ public void testChangeLayout1b() throws Exception {
+ // Same as 1a, but with different formatting to look for edit handling to for example
+ // remove a line that is made empty when its only attribute is removed
+ checkRefactoring("sample1b.xml", true);
+ }
+
+ public void testChangeLayout2() throws Exception {
+ // Test code which analyzes an embedded RelativeLayout
+ checkRefactoring("sample2.xml", true);
+ }
+
+ public void testChangeLayout3() throws Exception {
+ // Test handling of LinearLayout "weight" attributes on its children: the child with
+ // weight > 0 should fill and subsequent children attach on the bottom/right
+ checkRefactoring("sample3.xml", true);
+ }
+
+ public void testChangeLayout4() throws Exception {
+ checkRefactoring("sample4.xml", true);
+ }
+
+ public void testChangeLayout5() throws Exception {
+ // Test handling of LinearLayout "gravity" attributes on its children
+ checkRefactoring("sample5.xml", true);
+ }
+
+ public void testChangeLayout6() throws Exception {
+ // Check handling of the LinearLayout "baseline" attribute
+ checkRefactoring("sample6.xml", true);
+ }
+
+ private void checkRefactoring(String basename, boolean flatten) throws Exception {
+ IFile file = getTestFile(sProject, basename);
+ TestContext info = setupTestContext(file, basename);
+ TestLayoutEditor layoutEditor = info.mLayoutEditor;
+ CanvasViewInfo rootView = info.mRootView;
+ Element element = info.mElement;
+
+ List<Element> selectedElements = Collections.singletonList(element);
+ ChangeLayoutRefactoring refactoring = new ChangeLayoutRefactoring(selectedElements,
+ layoutEditor);
+ refactoring.setFlatten(flatten);
+ refactoring.setType(FQCN_RELATIVE_LAYOUT);
+ refactoring.setRootView(rootView);
+
+ List<Change> changes = refactoring.computeChanges();
+ checkEdits(basename, changes);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoringTest.java
new file mode 100644
index 0000000..6cc92d5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeViewRefactoringTest.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 static com.android.ide.common.layout.LayoutConstants.FQCN_RADIO_BUTTON;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class ChangeViewRefactoringTest extends RefactoringTest {
+
+ public void testChangeView1() throws Exception {
+ checkRefactoring("sample1a.xml", "@+id/button1", "@+id/button6");
+ }
+
+ private void checkRefactoring(String basename, String... ids) throws Exception {
+ assertTrue(ids.length > 0);
+
+ IFile file = getTestFile(sProject, basename);
+ TestContext info = setupTestContext(file, basename);
+ TestLayoutEditor layoutEditor = info.mLayoutEditor;
+ List<Element> selectedElements = getElements(info.mElement, ids);
+
+ ChangeViewRefactoring refactoring = new ChangeViewRefactoring(selectedElements,
+ layoutEditor);
+ refactoring.setType(FQCN_RADIO_BUTTON);
+
+ List<Change> changes = refactoring.computeChanges();
+ checkEdits(basename, changes);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoringTest.java
new file mode 100644
index 0000000..0e96b34
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoringTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.eclipse.adt.AdtConstants.DOT_XML;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.w3c.dom.Element;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ExtractIncludeRefactoringTest extends RefactoringTest {
+
+ public void testExtract1() throws Exception {
+ // Basic: Extract a single button
+ checkRefactoring("sample3.xml", "newlayout1", true, true, "@+id/button2");
+ }
+
+ public void testExtract2() throws Exception {
+ // Extract a couple of elements
+ checkRefactoring("sample3.xml", "newlayout2", true, true,
+ "@+id/button2", "@+id/android_logo");
+ }
+
+ private void checkRefactoring(String basename, String layoutName,
+ boolean updateRefs, boolean replaceOccurrences, String... ids) throws Exception {
+ assertTrue(ids.length > 0);
+
+ IFile file = getTestFile(sProject, basename);
+ TestContext info = setupTestContext(file, basename);
+ TestLayoutEditor layoutEditor = info.mLayoutEditor;
+ List<Element> selectedElements = getElements(info.mElement, ids);
+
+ ExtractIncludeRefactoring refactoring = new ExtractIncludeRefactoring(selectedElements,
+ layoutEditor);
+ refactoring.setLayoutName(layoutName);
+ refactoring.setUpdateReferences(updateRefs);
+ refactoring.setReplaceOccurrences(replaceOccurrences);
+ List<Change> changes = refactoring.computeChanges();
+
+ assertEquals(3, changes.size());
+
+ // The first change is to update the current file:
+ checkEdits(basename, Collections.singletonList(changes.get(0)));
+
+ // The second change is to create the new extracted file
+ checkEdits(layoutName + DOT_XML, Collections.singletonList(changes.get(1)));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringTest.java
new file mode 100644
index 0000000..0f7b66a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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_WIDGET_PREFIX;
+import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;
+
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@SuppressWarnings("restriction")
+public class RefactoringTest extends AdtProjectTest {
+ protected static Element findElementById(Element root, String id) {
+ if (id.equals(VisualRefactoring.getId(root))) {
+ return root;
+ }
+
+ for (Element child : RelativeLayoutConversionHelper.getChildren(root)) {
+ Element result = findElementById(child, id);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ protected static List<Element> getElements(Element root, String... ids) {
+ List<Element> selectedElements = new ArrayList<Element>();
+ for (String id : ids) {
+ Element element = findElementById(root, id);
+ assertNotNull(element);
+ selectedElements.add(element);
+ }
+ return selectedElements;
+ }
+
+ protected void checkEdits(String basename, List<Change> changes) throws BadLocationException,
+ IOException {
+ IDocument document = new Document();
+
+ String xml = readTestFile(basename, false);
+ if (xml == null) { // New file
+ xml = ""; //$NON-NLS-1$
+ }
+ document.set(xml);
+
+ for (Change change : changes) {
+ if (change instanceof TextFileChange) {
+ TextFileChange tf = (TextFileChange) change;
+ TextEdit edit = tf.getEdit();
+ if (edit instanceof MultiTextEdit) {
+ MultiTextEdit edits = (MultiTextEdit) edit;
+ edits.apply(document);
+ } else {
+ edit.apply(document);
+ }
+ } else {
+ System.out.println("Ignoring non-textfilechange in refactoring result");
+ }
+ }
+
+ String actual = document.get();
+ assertEqualsGolden(basename, actual);
+ }
+
+ protected void assertEqualsGolden(String basename, String actual) {
+ String testName = getName();
+ if (testName.startsWith("test")) {
+ testName = testName.substring(4);
+ if (Character.isUpperCase(testName.charAt(0))) {
+ testName = Character.toLowerCase(testName.charAt(0)) + testName.substring(1);
+ }
+ }
+ String expectedName;
+ if (basename.endsWith(DOT_XML)) {
+ expectedName = basename.substring(0, basename.length() - DOT_XML.length())
+ + "-expected-" + testName + DOT_XML;
+ } else {
+ expectedName = basename + ".expected";
+ }
+ String expected = readTestFile(expectedName, false);
+ if (expected == null) {
+ File expectedPath = new File(getTempDir(), expectedName);
+ AdtPlugin.writeFile(expectedPath, actual);
+ System.out.println("Expected - written to " + expectedPath + ":\n");
+ System.out.println(actual);
+ fail("Did not find golden file (" + expectedName + "): Wrote contents as "
+ + expectedPath);
+ } else {
+ if (!expected.equals(actual)) {
+ File expectedPath = new File(getTempDir(), expectedName);
+ File actualPath = new File(getTempDir(),
+ expectedName.replace("expected", "actual"));
+ AdtPlugin.writeFile(expectedPath, expected);
+ AdtPlugin.writeFile(actualPath, actual);
+ System.out.println("The files differ - see " + expectedPath + " versus "
+ + actualPath);
+ assertEquals("The files differ - see " + expectedPath + " versus " + actualPath,
+ expected, actual);
+ }
+ }
+ }
+
+ protected UiViewElementNode createModel(UiViewElementNode parent, Element element) {
+ List<Element> children = RelativeLayoutConversionHelper.getChildren(element);
+ String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName();
+ boolean hasChildren = children.size() > 0;
+ UiViewElementNode node = createNode(parent, fqcn, hasChildren);
+ node.setXmlNode(element);
+ for (Element child : children) {
+ createModel(node, child);
+ }
+
+ return node;
+ }
+
+ /**
+ * Builds up a ViewInfo hierarchy for the given model. This is done by
+ * reading .info dump files which record the exact pixel sizes of each
+ * ViewInfo object. These files are assumed to match up exactly with the
+ * model objects. This is done rather than rendering an actual layout
+ * hierarchy to insulate the test from pixel difference (in say font size)
+ * among platforms, as well as tying the test to particulars about relative
+ * sizes of things which may change with theme adjustments etc.
+ * <p>
+ * Each file can be generated by the dump method in the ViewHierarchy.
+ */
+ protected ViewInfo createInfos(UiElementNode model, String relativePath) {
+ String basename = relativePath.substring(0, relativePath.lastIndexOf('.') + 1);
+ String relative = basename + "info"; //$NON-NLS-1$
+ String info = readTestFile(relative, true);
+ // Parse the info file and build up a model from it
+ // Each line contains a new info.
+ // If indented it is a child of the parent.
+ String[] lines = info.split("\n"); //$NON-NLS-1$
+
+ // Iteration order for the info file should match exactly the UI model so
+ // we can just advance the line index sequentially as we traverse
+
+ return create(model, Arrays.asList(lines).iterator());
+ }
+
+ protected ViewInfo create(UiElementNode node, Iterator<String> lineIterator) {
+ // android.widget.LinearLayout [0,36,240,320]
+ Pattern pattern = Pattern.compile("(\\s*)(\\S+) \\[(\\d+),(\\d+),(\\d+),(\\d+)\\].*");
+ assertTrue(lineIterator.hasNext());
+ String description = lineIterator.next();
+ Matcher matcher = pattern.matcher(description);
+ assertTrue(matcher.matches());
+ //String indent = matcher.group(1);
+ //String fqcn = matcher.group(2);
+ String left = matcher.group(3);
+ String top = matcher.group(4);
+ String right = matcher.group(5);
+ String bottom = matcher.group(6);
+
+ ViewInfo view = new ViewInfo(node.getXmlNode().getLocalName(), node,
+ Integer.parseInt(left), Integer.parseInt(top),
+ Integer.parseInt(right), Integer.parseInt(bottom));
+
+ List<UiElementNode> childNodes = node.getUiChildren();
+ if (childNodes.size() > 0) {
+ List<ViewInfo> children = new ArrayList<ViewInfo>();
+ for (UiElementNode child : childNodes) {
+ children.add(create(child, lineIterator));
+ }
+ view.setChildren(children);
+ }
+
+ return view;
+ }
+
+ protected File getTempDir() {
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ return new File("/tmp"); //$NON-NLS-1$
+ }
+ return new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$
+ }
+
+ protected TestContext setupTestContext(IFile file, String relativePath) throws Exception {
+ IStructuredModel structuredModel = null;
+ org.w3c.dom.Document domDocument = null;
+ IStructuredDocument structuredDocument = null;
+ Element element = null;
+
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ structuredModel = modelManager.getModelForRead(file);
+ if (structuredModel instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) structuredModel;
+ domDocument = domModel.getDocument();
+ element = domDocument.getDocumentElement();
+ structuredDocument = structuredModel.getStructuredDocument();
+ }
+ } finally {
+ if (structuredModel != null) {
+ structuredModel.releaseFromRead();
+ }
+ }
+
+ assertNotNull(structuredModel);
+ assertNotNull(domDocument);
+ assertNotNull(element);
+ assertNotNull(structuredDocument);
+ assertTrue(element instanceof IndexedRegion);
+
+ UiViewElementNode model = createModel(null, element);
+ ViewInfo info = createInfos(model, relativePath);
+ CanvasViewInfo rootView = CanvasViewInfo.create(info).getFirst();
+ TestLayoutEditor layoutEditor = new TestLayoutEditor(file, structuredDocument, null);
+
+ TestContext testInfo = createTestContext();
+ testInfo.mFile = file;
+ testInfo.mStructuredModel = structuredModel;
+ testInfo.mStructuredDocument = structuredDocument;
+ testInfo.mElement = element;
+ testInfo.mDomDocument = domDocument;
+ testInfo.mUiModel = model;
+ testInfo.mViewInfo = info;
+ testInfo.mRootView = rootView;
+ testInfo.mLayoutEditor = layoutEditor;
+
+ return testInfo;
+ }
+
+ protected TestContext createTestContext() {
+ return new TestContext();
+ }
+
+ protected static class TestContext {
+ protected IFile mFile;
+ protected IStructuredModel mStructuredModel;
+ protected IStructuredDocument mStructuredDocument;
+ protected org.w3c.dom.Document mDomDocument;
+ protected Element mElement;
+ protected UiViewElementNode mUiModel;
+ protected ViewInfo mViewInfo;
+ protected CanvasViewInfo mRootView;
+ protected TestLayoutEditor mLayoutEditor;
+ }
+
+ @Override
+ public void testDummy() {
+ // To avoid JUnit warning that this class contains no tests, even though
+ // this is an abstract class and JUnit shouldn't try
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoringTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoringTest.java
new file mode 100644
index 0000000..f8238e1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoringTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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_GESTURE_OVERLAY_VIEW;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_LINEAR_LAYOUT;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class WrapInRefactoringTest extends RefactoringTest {
+
+ public void testWrapIn1() throws Exception {
+ // Test wrapping view: should indent view
+ checkRefactoring("sample3.xml", FQCN_LINEAR_LAYOUT, "@+id/button2");
+ }
+
+ public void testWrapIn2() throws Exception {
+ // Test wrapping the root: should move namespace
+ checkRefactoring("sample3.xml", FQCN_GESTURE_OVERLAY_VIEW, "@+id/newlinear");
+ }
+
+ public void testWrapIn3() throws Exception {
+ // Test wrap multiple adjacent elements - should wrap all as a unit
+ checkRefactoring("sample3.xml", FQCN_LINEAR_LAYOUT, "@+id/button2", "@+id/android_logo");
+ }
+
+ private void checkRefactoring(String basename, String fqcn, String... ids) throws Exception {
+ assertTrue(ids.length > 0);
+
+ IFile file = getTestFile(sProject, basename);
+ TestContext info = setupTestContext(file, basename);
+ TestLayoutEditor layoutEditor = info.mLayoutEditor;
+ List<Element> selectedElements = getElements(info.mElement, ids);
+
+ WrapInRefactoring refactoring = new WrapInRefactoring(selectedElements,
+ layoutEditor);
+ refactoring.setType(fqcn);
+ List<Change> changes = refactoring.computeChanges();
+ checkEdits(basename, changes);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout1-expected-extract1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout1-expected-extract1.xml
new file mode 100644
index 0000000..2a0e947
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout1-expected-extract1.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Button xmlns:android="http://schemas.android.com/apk/res/android" android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout2-expected-extract2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout2-expected-extract2.xml
new file mode 100644
index 0000000..b2d1789
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/newlayout2-expected-extract2.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</merge>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeLayout1a.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeLayout1a.xml
new file mode 100644
index 0000000..98e0a8f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeLayout1a.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/RelativeLayout1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="FirstButton" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button2" android:layout_below="@+id/button1" android:layout_marginTop="2dip" android:text="SecondButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button2"></Button>
+ <Button android:layout_toRightOf="@+id/button2" android:layout_alignBaseline="@+id/button2" android:layout_alignTop="@+id/button2" android:text="ThirdButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button3"></Button>
+ <CheckBox android:layout_toRightOf="@+id/button3" android:layout_alignBaseline="@+id/button2" android:layout_below="@+id/button1" android:id="@+id/checkBox1" android:text="CheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content"></CheckBox>
+ <Button android:layout_alignParentLeft="true" android:layout_below="@+id/checkBox1" android:layout_height="wrap_content" android:text="FourthButton" android:id="@+id/button4" android:layout_width="match_parent"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button5" android:layout_below="@+id/button4" android:layout_gravity="right" android:id="@+id/button5" android:text="FifthButton" android:layout_height="wrap_content" android:layout_width="wrap_content"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button6" android:layout_below="@+id/button5" android:text="Button" android:id="@+id/button6" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeView1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeView1.xml
new file mode 100644
index 0000000..f4a08d0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-changeView1.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout android:id="@+id/LinearLayout2" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <RadioButton android:text="FirstButton" android:id="@+id/RadioButton1" android:layout_width="wrap_content" android:layout_height="wrap_content"></RadioButton>
+ <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout1" android:layout_height="wrap_content">
+ <Button android:text="SecondButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button2"></Button>
+ <Button android:text="ThirdButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button3"></Button>
+ <CheckBox android:id="@+id/checkBox1" android:text="CheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content"></CheckBox>
+ </LinearLayout>
+ <Button android:layout_height="wrap_content" android:text="FourthButton" android:id="@+id/button4" android:layout_width="match_parent"></Button>
+ <LinearLayout android:layout_height="wrap_content" android:id="@+id/linearLayout3" android:layout_width="match_parent">
+ <Button android:layout_gravity="right" android:id="@+id/button5" android:text="FifthButton" android:layout_height="wrap_content" android:layout_width="wrap_content"></Button>
+ </LinearLayout>
+ <LinearLayout android:layout_height="wrap_content" android:id="@+id/linearLayout4" android:layout_width="match_parent">
+ <LinearLayout android:layout_height="match_parent" android:id="@+id/linearLayout5" android:layout_width="wrap_content">
+ <LinearLayout android:layout_height="match_parent" android:id="@+id/linearLayout6" android:layout_width="wrap_content">
+ <RadioButton android:text="Button" android:id="@+id/RadioButton2" android:layout_width="wrap_content" android:layout_height="wrap_content"></RadioButton>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.info
new file mode 100644
index 0000000..20a23b0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.info
@@ -0,0 +1,13 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [0,0,70,36] <Button>
+ android.widget.LinearLayout [0,36,240,72] <LinearLayout>
+ android.widget.Button [0,2,84,38] <Button>
+ android.widget.Button [84,2,158,38] <Button>
+ android.widget.CheckBox [158,0,238,36] <CheckBox>
+ android.widget.Button [0,72,240,108] <Button>
+ android.widget.LinearLayout [0,108,240,144] <LinearLayout>
+ android.widget.Button [0,0,71,36] <Button>
+ android.widget.LinearLayout [0,144,240,180] <LinearLayout>
+ android.widget.LinearLayout [0,0,49,36] <LinearLayout>
+ android.widget.LinearLayout [0,0,49,36] <LinearLayout>
+ android.widget.Button [0,0,49,36] <Button>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.xml
new file mode 100644
index 0000000..9a94935
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout android:id="@+id/LinearLayout2" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:text="FirstButton" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout1" android:layout_height="wrap_content">
+ <Button android:text="SecondButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button2"></Button>
+ <Button android:text="ThirdButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button3"></Button>
+ <CheckBox android:id="@+id/checkBox1" android:text="CheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content"></CheckBox>
+ </LinearLayout>
+ <Button android:layout_height="wrap_content" android:text="FourthButton" android:id="@+id/button4" android:layout_width="match_parent"></Button>
+ <LinearLayout android:layout_height="wrap_content" android:id="@+id/linearLayout3" android:layout_width="match_parent">
+ <Button android:layout_gravity="right" android:id="@+id/button5" android:text="FifthButton" android:layout_height="wrap_content" android:layout_width="wrap_content"></Button>
+ </LinearLayout>
+ <LinearLayout android:layout_height="wrap_content" android:id="@+id/linearLayout4" android:layout_width="match_parent">
+ <LinearLayout android:layout_height="match_parent" android:id="@+id/linearLayout5" android:layout_width="wrap_content">
+ <LinearLayout android:layout_height="match_parent" android:id="@+id/linearLayout6" android:layout_width="wrap_content">
+ <Button android:text="Button" android:id="@+id/button6" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b-expected-changeLayout1b.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b-expected-changeLayout1b.xml
new file mode 100644
index 0000000..737e72d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b-expected-changeLayout1b.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ android:id="@+id/RelativeLayout1"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <Button android:layout_alignParentLeft="true" android:layout_alignParentTop="true"
+ android:text="FirstButton"
+ android:id="@+id/button1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button2" android:layout_below="@+id/button1" android:layout_marginTop="2dip"
+ android:text="SecondButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/button2"></Button>
+ <Button android:layout_toRightOf="@+id/button2" android:layout_alignBaseline="@+id/button2" android:layout_alignTop="@+id/button2"
+ android:text="ThirdButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/button3"></Button>
+ <CheckBox android:layout_toRightOf="@+id/button3" android:layout_alignBaseline="@+id/button2" android:layout_below="@+id/button1"
+ android:id="@+id/checkBox1"
+ android:text="CheckBox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></CheckBox>
+ <Button android:layout_alignParentLeft="true" android:layout_below="@+id/checkBox1"
+ android:layout_height="wrap_content"
+ android:text="FourthButton"
+ android:id="@+id/button4"
+ android:layout_width="match_parent"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button5" android:layout_below="@+id/button4"
+ android:layout_gravity="right"
+ android:id="@+id/button5"
+ android:text="FifthButton"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button6" android:layout_below="@+id/button5"
+ android:text="Button"
+ android:id="@+id/button6"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.info
new file mode 100644
index 0000000..7807227
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.info
@@ -0,0 +1,14 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [0,0,70,36] <Button>
+ android.widget.LinearLayout [0,36,240,72] <LinearLayout>
+ android.widget.Button [0,2,84,38] <Button>
+ android.widget.Button [84,2,158,38] <Button>
+ android.widget.CheckBox [158,0,238,36] <CheckBox>
+ android.widget.Button [0,72,240,108] <Button>
+ android.widget.LinearLayout [0,108,240,144] <LinearLayout>
+ android.widget.Button [0,0,71,36] <Button>
+ android.widget.LinearLayout [0,144,240,180] <LinearLayout>
+ android.widget.LinearLayout [0,0,49,36] <LinearLayout>
+ android.widget.LinearLayout [0,0,49,36] <LinearLayout>
+ android.widget.Button [0,0,49,36] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.xml
new file mode 100644
index 0000000..6b800ae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1b.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ android:id="@+id/LinearLayout2"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <Button
+ android:text="FirstButton"
+ android:id="@+id/button1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></Button>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:id="@+id/linearLayout1"
+ android:layout_height="wrap_content">
+ <Button
+ android:text="SecondButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/button2"></Button>
+ <Button
+ android:text="ThirdButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/button3"></Button>
+ <CheckBox
+ android:id="@+id/checkBox1"
+ android:text="CheckBox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></CheckBox>
+ </LinearLayout>
+ <Button
+ android:layout_height="wrap_content"
+ android:text="FourthButton"
+ android:id="@+id/button4"
+ android:layout_width="match_parent"></Button>
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:id="@+id/linearLayout3"
+ android:layout_width="match_parent">
+ <Button
+ android:layout_gravity="right"
+ android:id="@+id/button5"
+ android:text="FifthButton"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"></Button>
+ </LinearLayout>
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:id="@+id/linearLayout4"
+ android:layout_width="match_parent">
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:id="@+id/linearLayout5"
+ android:layout_width="wrap_content">
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:id="@+id/linearLayout6"
+ android:layout_width="wrap_content">
+ <Button
+ android:text="Button"
+ android:id="@+id/button6"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"></Button>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeLayout2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeLayout2.xml
new file mode 100644
index 0000000..cce3803
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2-expected-changeLayout2.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/RelativeLayout1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <Button android:layout_below="@+id/button1" android:layout_width="wrap_content" android:layout_alignParentLeft="true" android:id="@+id/button2" android:layout_height="wrap_content" android:text="Button"></Button>
+ <Button android:layout_width="wrap_content" android:id="@+id/button3" android:layout_below="@+id/button2" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/button2"></Button>
+ <Button android:layout_width="wrap_content" android:id="@+id/button4" android:layout_below="@+id/button3" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/button3"></Button>
+ <CheckBox android:layout_width="wrap_content" android:layout_below="@+id/button4" android:id="@+id/checkBox1" android:layout_height="wrap_content" android:text="CheckBox" android:layout_toLeftOf="@+id/button4"></CheckBox>
+ <Button android:layout_alignTop="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button5" android:layout_alignParentRight="true"></Button>
+</RelativeLayout>
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.info
new file mode 100644
index 0000000..44d3b62
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.info
@@ -0,0 +1,9 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [0,0,49,36] <Button>
+ android.widget.RelativeLayout [0,36,240,284] <RelativeLayout>
+ android.widget.Button [0,0,49,36] <Button>
+ android.widget.Button [49,36,98,72] <Button>
+ android.widget.Button [98,72,147,108] <Button>
+ android.widget.CheckBox [18,108,98,144] <CheckBox>
+ android.widget.Button [191,0,240,36] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.xml
new file mode 100644
index 0000000..0697c64
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <RelativeLayout android:layout_height="match_parent" android:id="@+id/relativeLayout1" android:layout_width="match_parent">
+ <Button android:layout_width="wrap_content" android:layout_alignParentLeft="true" android:id="@+id/button2" android:layout_height="wrap_content" android:text="Button"></Button>
+ <Button android:layout_width="wrap_content" android:id="@+id/button3" android:layout_below="@+id/button2" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/button2"></Button>
+ <Button android:layout_width="wrap_content" android:id="@+id/button4" android:layout_below="@+id/button3" android:layout_height="wrap_content" android:text="Button" android:layout_toRightOf="@+id/button3"></Button>
+ <CheckBox android:layout_width="wrap_content" android:layout_below="@+id/button4" android:id="@+id/checkBox1" android:layout_height="wrap_content" android:text="CheckBox" android:layout_toLeftOf="@+id/button4"></CheckBox>
+ <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button5" android:layout_alignParentRight="true"></Button>
+ </RelativeLayout>
+</LinearLayout>
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-changeLayout3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-changeLayout3.xml
new file mode 100644
index 0000000..c5118b6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-changeLayout3.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <ImageView android:layout_alignParentLeft="true" android:layout_below="@+id/button1" android:layout_above="@+id/button2" android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <Button android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract1.xml
new file mode 100644
index 0000000..70f576e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <include layout="@layout/newlayout1" android:id="@+id/button2_ref" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract2.xml
new file mode 100644
index 0000000..9a30a96
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-extract2.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <include layout="@layout/newlayout2" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn1.xml
new file mode 100644
index 0000000..5af18d6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn2.xml
new file mode 100644
index 0000000..5d5fbfc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn2.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.gesture.GestureOverlayView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ </LinearLayout>
+</android.gesture.GestureOverlayView>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn3.xml
new file mode 100644
index 0000000..efbeb18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3-expected-wrapIn3.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.info
new file mode 100644
index 0000000..ef3324e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.info
@@ -0,0 +1,5 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [0,0,49,36] <Button>
+ android.widget.ImageView [0,36,128,248] <ImageView>
+ android.widget.Button [0,248,49,284] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.xml
new file mode 100644
index 0000000..ddd136c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample3.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/newlinear" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <ImageView android:id="@+id/android_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/android_button" android:focusable="false" android:clickable="false" android:layout_weight="1.0" />
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4-expected-changeLayout4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4-expected-changeLayout4.xml
new file mode 100644
index 0000000..2dd7ab7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4-expected-changeLayout4.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/RelativeLayout1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <CheckBox android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="CheckBox" android:id="@+id/checkBox1" android:layout_width="wrap_content" android:layout_height="wrap_content"></CheckBox>
+ <Button android:layout_below="@+id/checkBox1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button5" android:layout_alignParentRight="true"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.info
new file mode 100644
index 0000000..605c177
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.info
@@ -0,0 +1,5 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.CheckBox [0,0,80,36] <CheckBox>
+ android.widget.RelativeLayout [0,36,240,284] <RelativeLayout>
+ android.widget.Button [191,0,240,36] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.xml
new file mode 100644
index 0000000..a56f272
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample4.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <CheckBox android:text="CheckBox" android:id="@+id/checkBox1" android:layout_width="wrap_content" android:layout_height="wrap_content"></CheckBox>
+ <RelativeLayout android:layout_height="match_parent" android:id="@+id/relativeLayout1" android:layout_width="match_parent">
+ <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button5" android:layout_alignParentRight="true"></Button>
+ </RelativeLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-changeLayout5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-changeLayout5.xml
new file mode 100644
index 0000000..fba3049
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5-expected-changeLayout5.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout android:id="@+id/RelativeLayout1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:layout_centerHorizontal="true" android:layout_alignParentTop="true" android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal"></Button>
+ <Button android:layout_centerHorizontal="true" android:layout_below="@+id/button1" android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"></Button>
+ <Button android:layout_alignParentRight="true" android:layout_below="@+id/button2" android:text="Button" android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right"></Button>
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button4" android:layout_below="@+id/button3" android:layout_marginTop="70dip" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Button" android:id="@+id/button4" android:layout_width="wrap_content"></Button>
+ <Button android:layout_toRightOf="@+id/button4" android:layout_alignBaseline="@+id/button4" android:layout_alignTop="@+id/button4" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="Button" android:id="@+id/button5" android:layout_width="wrap_content"></Button>
+ <Button android:layout_toRightOf="@+id/button5" android:layout_alignParentBottom="true" android:layout_height="wrap_content" android:text="Button" android:id="@+id/button6" android:layout_width="wrap_content" android:layout_gravity="bottom"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.info
new file mode 100644
index 0000000..fce532e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.info
@@ -0,0 +1,9 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [95,0,144,36] <Button>
+ android.widget.Button [95,36,144,72] <Button>
+ android.widget.Button [191,72,240,108] <Button>
+ android.widget.LinearLayout [0,108,240,284] <LinearLayout>
+ android.widget.Button [0,70,49,106] <Button>
+ android.widget.Button [49,70,98,106] <Button>
+ android.widget.Button [98,140,147,176] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.xml
new file mode 100644
index 0000000..afc1938
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample5.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal"></Button>
+ <Button android:text="Button" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"></Button>
+ <Button android:text="Button" android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right"></Button>
+ <LinearLayout android:id="@+id/linearLayout1" android:layout_width="match_parent" android:layout_height="match_parent">
+ <Button android:layout_height="wrap_content" android:layout_gravity="center" android:text="Button" android:id="@+id/button4" android:layout_width="wrap_content"></Button>
+ <Button android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="Button" android:id="@+id/button5" android:layout_width="wrap_content"></Button>
+ <Button android:layout_height="wrap_content" android:text="Button" android:id="@+id/button6" android:layout_width="wrap_content" android:layout_gravity="bottom"></Button>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6-expected-changeLayout6.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6-expected-changeLayout6.xml
new file mode 100644
index 0000000..045d2ce
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6-expected-changeLayout6.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/RelativeLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:baselineAligned="true">
+ <Button android:layout_alignParentLeft="true" android:layout_alignBaseline="@+id/button2" android:layout_alignParentTop="true" android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <RadioButton android:layout_toRightOf="@+id/button1" android:layout_alignBaseline="@+id/button2" android:layout_alignParentTop="true" android:text="RadioButton" android:id="@+id/radioButton1" android:layout_width="wrap_content" android:layout_height="wrap_content"></RadioButton>
+ <Button android:layout_toRightOf="@+id/radioButton1" android:layout_alignBaseline="@+id/button2" android:layout_alignParentTop="true" android:layout_width="wrap_content" android:id="@+id/button2" android:text="Button" android:layout_height="150dip"></Button>
+</RelativeLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.info b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.info
new file mode 100644
index 0000000..7a7a44a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.info
@@ -0,0 +1,5 @@
+android.widget.LinearLayout [0,36,240,320] <LinearLayout>
+ android.widget.Button [0,38,49,74] <Button>
+ android.widget.RadioButton [49,36,143,72] <RadioButton>
+ android.widget.Button [143,0,192,113] <Button>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.xml
new file mode 100644
index 0000000..5cdc824
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample6.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/LinearLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:baselineAligned="true">
+ <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
+ <RadioButton android:text="RadioButton" android:id="@+id/radioButton1" android:layout_width="wrap_content" android:layout_height="wrap_content"></RadioButton>
+ <Button android:layout_width="wrap_content" android:id="@+id/button2" android:text="Button" android:layout_height="150dip"></Button>
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java
index 2774557..d0998c9 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/SdkTestCase.java
@@ -17,6 +17,7 @@ package com.android.ide.eclipse.tests;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetParser;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.IAndroidTarget;
@@ -60,6 +61,13 @@ public abstract class SdkTestCase extends TestCase {
return null;
}
+ // We'll never break out of the SDK load-wait-loop if the AdtPlugin doesn't
+ // actually have a valid SDK location because it won't have started an async load:
+ String sdkLocation = AdtPrefs.getPrefs().getOsSdkFolder();
+ assertTrue("No valid SDK installation is set; for tests you typically need to set the"
+ + " environment variable ADT_TEST_SDK_PATH to point to an SDK folder",
+ sdkLocation != null && sdkLocation.length() > 0);
+
Object sdkLock = Sdk.getLock();
LoadStatus loadStatus = LoadStatus.LOADING;
// wait for ADT to load the SDK on a separate thread
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
index 5b79ac3..9c60d33 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java
@@ -39,7 +39,7 @@ import junit.framework.TestCase;
/**
* Common layout helpers from LayoutRule tests
*/
-public abstract class LayoutTestBase extends TestCase {
+public class LayoutTestBase extends TestCase {
/**
* Helper function used by tests to drag a button into a canvas containing
* the given children.
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java
index 5e86d69..91d0e13 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java
@@ -29,7 +29,7 @@ import junit.framework.TestCase;
* Common utilities for the point tests {@link LayoutPointTest} and
* {@link ControlPointTest}
*/
-public abstract class PointTestCases extends TestCase {
+public class PointTestCases extends TestCase {
LayoutCanvas mCanvas = new TestLayoutCanvas();
protected MouseEvent canvasMouseEvent(int x, int y, int stateMask) {
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java
index d18967d..36f4ccd 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepositoryTest.java
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gre;
import com.android.ide.common.api.IViewMetadata.FillPreference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
import junit.framework.TestCase;
@@ -32,4 +33,31 @@ public class ViewMetadataRepositoryTest extends TestCase {
assertEquals(FillPreference.NONE,
repository.getFillPreference("foo.bar"));
}
+
+ // Ensure that all basenames referenced in the metadata refer to other views in the file
+ // (e.g. no typos)
+ public void testRelatedTo() throws Exception {
+ // Make sure unit tests are run with assertions on
+ boolean assertionsEnabled = false;
+ assert assertionsEnabled = true; // Intentional assignment
+ assertTrue("This unit test must be run with assertions enabled (-ea)", assertionsEnabled);
+
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ for (String fqcn : repository.getAllFqcns()) {
+ repository.getRelatedTo(fqcn);
+ }
+ }
+
+ public void testSkip() throws Exception {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ assertTrue(repository.getSkip("include"));
+ assertFalse(repository.getSkip("android.widget.Button"));
+ }
+
+ public void testRenderMode() throws Exception {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ assertEquals(RenderMode.NORMAL, repository.getRenderMode("android.widget.Button"));
+ assertEquals(RenderMode.SKIP, repository.getRenderMode("android.widget.LinearLayout"));
+ assertEquals(RenderMode.ALONE, repository.getRenderMode("android.widget.TabHost"));
+ }
}