diff options
167 files changed, 8164 insertions, 620 deletions
diff --git a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java index 6ad98ad..285d922 100644 --- a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java +++ b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java @@ -170,7 +170,11 @@ public class HierarchyViewer { } ViewNode.Property textProperty = node.namedProperties.get("text:mText"); if (textProperty == null) { - throw new RuntimeException("No text property on node"); + // give it another chance, ICS ViewServer returns mText + textProperty = node.namedProperties.get("mText"); + if (textProperty == null) { + throw new RuntimeException("No text property on node"); + } } return textProperty.value; } diff --git a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java index c2942d0..233c7c1 100644 --- a/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java +++ b/ddms/libs/ddmuilib/src/com/android/ddmuilib/logcat/LogCatPanel.java @@ -41,6 +41,8 @@ import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; @@ -52,6 +54,7 @@ import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; @@ -92,6 +95,15 @@ public final class LogCatPanel extends SelectionDependentPanel /** Preference key to use for storing font settings. */ public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; + // Preference keys for message colors based on severity level + private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color."; + public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$ + public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$ + public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$ + public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$ + public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$ + public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$ + // Use a monospace font family private static final String FONT_FAMILY = DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; @@ -163,6 +175,13 @@ public final class LogCatPanel extends SelectionDependentPanel private Font mFont; private int mWrapWidthInChars; + private Color mVerboseColor; + private Color mDebugColor; + private Color mInfoColor; + private Color mWarnColor; + private Color mErrorColor; + private Color mAssertColor; + private SashForm mSash; // messages added since last refresh, synchronized on mLogBuffer @@ -185,6 +204,20 @@ public final class LogCatPanel extends SelectionDependentPanel initializePreferenceUpdateListeners(); mFont = getFontFromPrefStore(); + loadMessageColorPreferences(); + } + + private void loadMessageColorPreferences() { + if (mVerboseColor != null) { + disposeMessageColors(); + } + + mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY); + mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY); + mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY); + mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY); + mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY); + mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY); } private void initializeFilters() { @@ -209,6 +242,20 @@ public final class LogCatPanel extends SelectionDependentPanel mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, LogCatMessageList.MAX_MESSAGES_DEFAULT); mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); + + /* Default Colors for different log levels. */ + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY, + new RGB(0, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY, + new RGB(0, 0, 127)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY, + new RGB(0, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY, + new RGB(255, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY, + new RGB(255, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY, + new RGB(255, 0, 0)); } private void initializePreferenceUpdateListeners() { @@ -230,6 +277,21 @@ public final class LogCatPanel extends SelectionDependentPanel } } }); + } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) { + loadMessageColorPreferences(); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Color c = mVerboseColor; + for (TableItem it: mTable.getItems()) { + Object data = it.getData(); + if (data instanceof LogCatMessage) { + c = getForegroundColor((LogCatMessage) data); + } + it.setForeground(c); + } + } + }); } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) { mReceiver.resizeFifo(mPrefStore.getInt( LogCatMessageList.MAX_MESSAGES_PREFKEY)); @@ -834,6 +896,13 @@ public final class LogCatPanel extends SelectionDependentPanel addRightClickMenu(mTable); initDoubleClickListener(); recomputeWrapWidth(); + + mTable.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent arg0) { + dispose(); + } + }); } /** Setup menu to be displayed when right clicking a log message. */ @@ -914,6 +983,11 @@ public final class LogCatPanel extends SelectionDependentPanel return new Font(Display.getDefault(), fd); } + private Color getColorFromPrefStore(String key) { + RGB rgb = PreferenceConverter.getColor(mPrefStore, key); + return new Color(Display.getDefault(), rgb); + } + private void setupDefaults() { int defaultFilterIndex = 0; mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); @@ -1239,29 +1313,24 @@ public final class LogCatPanel extends SelectionDependentPanel return wrappedMessages; } - /* Default Colors for different log levels. */ - private static final Color INFO_MSG_COLOR = new Color(null, 0, 127, 0); - private static final Color DEBUG_MSG_COLOR = new Color(null, 0, 0, 127); - private static final Color ERROR_MSG_COLOR = new Color(null, 255, 0, 0); - private static final Color WARN_MSG_COLOR = new Color(null, 255, 127, 0); - private static final Color VERBOSE_MSG_COLOR = new Color(null, 0, 0, 0); - - private static Color getForegroundColor(LogCatMessage m) { + private Color getForegroundColor(LogCatMessage m) { LogLevel l = m.getLogLevel(); if (l.equals(LogLevel.VERBOSE)) { - return VERBOSE_MSG_COLOR; + return mVerboseColor; } else if (l.equals(LogLevel.INFO)) { - return INFO_MSG_COLOR; + return mInfoColor; } else if (l.equals(LogLevel.DEBUG)) { - return DEBUG_MSG_COLOR; + return mDebugColor; } else if (l.equals(LogLevel.ERROR)) { - return ERROR_MSG_COLOR; + return mErrorColor; } else if (l.equals(LogLevel.WARN)) { - return WARN_MSG_COLOR; + return mWarnColor; + } else if (l.equals(LogLevel.ASSERT)) { + return mAssertColor; } - return null; + return mVerboseColor; } private List<ILogCatMessageSelectionListener> mMessageSelectionListeners; @@ -1344,4 +1413,23 @@ public final class LogCatPanel extends SelectionDependentPanel public void selectAll() { mTable.selectAll(); } + + private void dispose() { + if (mFont != null && !mFont.isDisposed()) { + mFont.dispose(); + } + + if (mVerboseColor != null && !mVerboseColor.isDisposed()) { + disposeMessageColors(); + } + } + + private void disposeMessageColors() { + mVerboseColor.dispose(); + mDebugColor.dispose(); + mInfoColor.dispose(); + mWarnColor.dispose(); + mErrorColor.dispose(); + mAssertColor.dispose(); + } } diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index fe8cc4b..dbb51e5 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -1,6 +1,7 @@ ' aapt accessor +accessors adb addon adt @@ -42,6 +43,7 @@ checkboxes classloader classpath clickable +clipart clipboard clipboards clueless @@ -279,16 +281,20 @@ subregion superclasses supertype symlinks +synced syncs synthetically tad temp +templated textfield textfields thematically themed thumbnail +thumbnails timestamp +timestamps tmp toolbar tooltip @@ -317,6 +323,7 @@ url urls utf validator +validators varargs verbosity viewport diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath index 4c132ed..e30e8c9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath @@ -17,6 +17,7 @@ <classpathentry exported="true" kind="lib" path="libs/ant-glob.jar"/> <classpathentry exported="true" kind="lib" path="libs/manifmerger.jar" sourcepath="/ManifestMerger"/> <classpathentry kind="var" path="ANDROID_SRC/sdk/eclipse/plugins/com.android.ide.eclipse.adt/libs/propertysheet.jar" sourcepath="/ANDROID_SRC/external/eclipse-windowbuilder/propertysheet/src"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/freemarker/freemarker-2.3.19.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/freemarker/src.zip"/> <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-tree-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/> <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/lombok-ast/src.zip"/> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index 38d2885..dc443db 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -7,6 +7,7 @@ Bundle-ClassPath: ., libs/sdkuilib.jar, libs/ninepatch.jar, libs/kxml2-2.3.0.jar, + libs/freemarker-2.3.19.jar, libs/layoutlib_api.jar, libs/ide_common.jar, libs/rule_api.jar, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png Binary files differnew file mode 100644 index 0000000..2e63c35 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error-badge.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png Binary files differnew file mode 100644 index 0000000..aa6f067 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint1.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png Binary files differnew file mode 100644 index 0000000..8434b78 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint2.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png Binary files differnew file mode 100644 index 0000000..43ab63e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint3.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png Binary files differnew file mode 100644 index 0000000..67fee2d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint4.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png Binary files differnew file mode 100644 index 0000000..3d0c188 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint5.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png Binary files differnew file mode 100644 index 0000000..42a9767 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint6.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png Binary files differnew file mode 100644 index 0000000..743aabc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint7.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png Binary files differnew file mode 100644 index 0000000..927067a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint8.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png Binary files differnew file mode 100644 index 0000000..44e66a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png Binary files differnew file mode 100644 index 0000000..853ace9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/lint9p.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png Binary files differnew file mode 100644 index 0000000..d71d6e3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning-badge.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index 5c3e9a0..1f402b9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -149,6 +149,21 @@ <category id="com.android.ide.eclipse.wizards.category" name="Android" /> + + <!-- New template-based project wizard --> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/new_adt_project.png" + id="ccom.android.ide.eclipse.adt.project.NewProjectWizard" + name="Android Application" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="true" /> + + <!-- Old custom-coded project wizard --> <wizard canFinishEarly="false" category="com.android.ide.eclipse.wizards.category" @@ -156,8 +171,8 @@ finalPerspective="org.eclipse.jdt.ui.JavaPerspective" hasPages="true" icon="icons/new_adt_project.png" - id="com.android.ide.eclipse.adt.project.NewProjectWizard" - name="Android Project" + id="com.android.ide.eclipse.adt.project.NewProjectWizard.Old" + name="Empty Android Project" preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" project="true" /> <wizard @@ -223,6 +238,39 @@ <wizard canFinishEarly="false" category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard$NewActivityWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/new_adt_project.png" + id="com.android.ide.eclipse.editors.wizards.NewTemplateWizard.Activity" + name="Blank Activity" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="false" /> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard$MasterDetailWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/new_adt_project.png" + id="com.android.ide.eclipse.editors.wizards.NewTemplateWizard.MasterDetail" + name="Master Detail Flow" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="false" /> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard$NewCustomViewWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/customView.png" + id="com.android.ide.eclipse.editors.wizards.NewTemplateWizard.CustomView" + name="Custom View" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="false" /> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" class="com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizard" finalPerspective="org.eclipse.jdt.ui.JavaPerspective" hasPages="true" @@ -231,6 +279,20 @@ name="Android Icon Set" preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" project="false" /> + + <!-- During development only: Support for testing templates --> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.templates.TemplateTestWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/androidjunit.png" + id="com.android.ide.eclipse.editors.wizards.TemplateTestWizard" + name="Template Development Wizard" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="false" /> + </extension> <extension point="org.eclipse.debug.core.launchConfigurationTypes"> <launchConfigurationType diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 75326fe..507bed5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -16,7 +16,6 @@ package com.android.ide.common.layout; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; import static com.android.ide.common.layout.LayoutConstants.ATTR_HINT; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; @@ -34,7 +33,10 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; +import static com.android.util.XmlUtils.ANDROID_URI; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.api.AbstractViewRule; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; @@ -971,7 +973,8 @@ public class BaseViewRule extends AbstractViewRule { * @param id attribute to be stripped * @return the id name without the {@code @+id} or {@code @id} prefix */ - public static String stripIdPrefix(String id) { + @NonNull + public static String stripIdPrefix(@Nullable String id) { if (id == null) { return ""; //$NON-NLS-1$ } else if (id.startsWith(NEW_ID_PREFIX)) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java index e5e84d9..cb5faba 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java @@ -125,6 +125,8 @@ public class AdtConstants { public static final String DOT_BMP = ".bmp"; //$NON-NLS-1$ /** Dot-Extension for SVG files, i.e. ".svg" */ public static final String DOT_SVG = ".svg"; //$NON-NLS-1$ + /** Dot-Extension for template files */ + public static final String DOT_FTL = ".ftl"; //$NON-NLS-1$ /** Name of the android sources directory */ public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index f8f6311..10cd093 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -47,6 +47,7 @@ import com.android.ide.eclipse.ddms.DdmsPlugin; import com.android.io.StreamException; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; import com.android.sdklib.SdkConstants; import org.eclipse.core.commands.Command; @@ -123,7 +124,7 @@ import java.util.List; /** * The activator class controls the plug-in life cycle */ -public class AdtPlugin extends AbstractUIPlugin implements ILogger { +public class AdtPlugin extends AbstractUIPlugin implements ILogger, ISdkLog { /** * Temporary logging code to help track down * http://code.google.com/p/android/issues/detail?id=15003 @@ -729,15 +730,18 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { InputStream is = readEmbeddedFileAsStream(filepath); if (is != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + try { + String line; + StringBuilder total = new StringBuilder(reader.readLine()); + while ((line = reader.readLine()) != null) { + total.append('\n'); + total.append(line); + } - String line; - StringBuilder total = new StringBuilder(reader.readLine()); - while ((line = reader.readLine()) != null) { - total.append('\n'); - total.append(line); + return total.toString(); + } finally { + reader.close(); } - - return total.toString(); } } catch (IOException e) { // we'll just return null @@ -759,16 +763,19 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { if (is != null) { // create a buffered reader to facilitate reading. BufferedInputStream stream = new BufferedInputStream(is); + try { + // get the size to read. + int avail = stream.available(); - // get the size to read. - int avail = stream.available(); - - // create the buffer and reads it. - byte[] buffer = new byte[avail]; - stream.read(buffer); + // create the buffer and reads it. + byte[] buffer = new byte[avail]; + stream.read(buffer); - // and return. - return buffer; + // and return. + return buffer; + } finally { + stream.close(); + } } } catch (IOException e) { // we'll just return null;. @@ -1853,6 +1860,8 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { openFile(file, region, true); } + // TODO: Make an openEditor which does the above, and make the above pass false for showEditor + /** * Opens the given file and shows the given (optional) region * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java index 8ba909f..09092e7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java @@ -25,6 +25,9 @@ import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; import com.android.util.XmlUtils; import org.eclipse.core.filesystem.URIUtil; @@ -36,6 +39,7 @@ import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IJavaProject; @@ -43,6 +47,8 @@ import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextUtilities; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; @@ -61,6 +67,7 @@ import java.io.File; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; @@ -167,7 +174,8 @@ public class AdtUtils { * @return the string as a Java class, or null if a class name could not be * extracted */ - public static String extractClassName(String string) { + @Nullable + public static String extractClassName(@NonNull String string) { StringBuilder sb = new StringBuilder(string.length()); int n = string.length(); @@ -264,6 +272,62 @@ public class AdtUtils { return sb.toString(); } + /** + * Converts a CamelCase word into an underlined_word + * + * @param string the CamelCase version of the word + * @return the underlined version of the word + */ + public static String camelCaseToUnderlines(String string) { + if (string.isEmpty()) { + return string; + } + + StringBuilder sb = new StringBuilder(2 * string.length()); + int n = string.length(); + boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0)); + for (int i = 0; i < n; i++) { + char c = string.charAt(i); + boolean isUpperCase = Character.isUpperCase(c); + if (isUpperCase && !lastWasUpperCase) { + sb.append('_'); + } + lastWasUpperCase = isUpperCase; + c = Character.toLowerCase(c); + sb.append(c); + } + + return sb.toString(); + } + + /** + * Converts an underlined_word into a CamelCase word + * + * @param string the underlined word to convert + * @return the CamelCase version of the word + */ + public static String underlinesToCamelCase(String string) { + StringBuilder sb = new StringBuilder(string.length()); + int n = string.length(); + + int i = 0; + boolean upcaseNext = true; + for (; i < n; i++) { + char c = string.charAt(i); + if (c == '_') { + upcaseNext = true; + } else { + if (upcaseNext) { + c = Character.toUpperCase(c); + } + upcaseNext = false; + sb.append(c); + } + } + + return sb.toString(); + } + /** For use by {@link #getLineSeparator()} */ private static String sLineSeparator; @@ -561,7 +625,10 @@ public class AdtUtils { String name = i == 1 ? base : base + conjunction + Integer.toString(i); boolean found = false; for (IProject project : projects) { - if (project.getName().equals(name)) { + // Need to make case insensitive comparison, since otherwise we can hit + // org.eclipse.core.internal.resources.ResourceException: + // A resource exists with a different case: '/test'. + if (project.getName().equalsIgnoreCase(name)) { found = true; break; } @@ -686,4 +753,160 @@ public class AdtUtils { }); } + /** + * Returns the Android version and code name of the given API level + * + * @param api the api level + * @return a suitable version display name + */ + public static String getAndroidName(int api) { + // See http://source.android.com/source/build-numbers.html + switch (api) { + case 1: return "API 1: Android 1.0"; + case 2: return "API 2: Android 1.1"; + case 3: return "API 3: Android 1.5 (Cupcake)"; + case 4: return "API 4: Android 1.6 (Donut)"; + case 5: return "API 5: Android 2.0 (Eclair)"; + case 6: return "API 6: Android 2.0.1 (Eclair)"; + case 7: return "API 7: Android 2.1 (Eclair)"; + case 8: return "API 8: Android 2.2 (Froyo)"; + case 9: return "API 9: Android 2.3 (Gingerbread)"; + case 10: return "API 10: Android 2.3.3 (Gingerbread)"; + case 11: return "API 11: Android 3.0 (Honeycomb)"; + case 12: return "API 12: Android 3.1 (Honeycomb)"; + case 13: return "API 13: Android 3.2 (Honeycomb)"; + case 14: return "API 14: Android 4.0 (IceCreamSandwich)"; + case 15: return "API 15: Android 4.0.3 (IceCreamSandwich)"; + default: { + // Consult SDK manager to see if we know any more (later) names, + // installed by user + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + for (IAndroidTarget target : sdk.getTargets()) { + if (target.isPlatform()) { + AndroidVersion version = target.getVersion(); + if (version.getApiLevel() == api) { + return version.getApiString(); + } + } + } + } + + return "API " + api; + + } + } + } + + /** + * Returns the highest known API level to this version of ADT. The + * {@link #getAndroidName(int)} method will return real names up to and + * including this number. + * + * @return the highest known API number + */ + public static int getHighestKnownApiLevel() { + return 15; + } + + /** + * Returns a list of known API names + * + * @return a list of string API names, starting from 1 and up through the + * maximum known versions (with no gaps) + */ + public static String[] getKnownVersions() { + int max = 15; + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + for (IAndroidTarget target : sdk.getTargets()) { + if (target.isPlatform()) { + AndroidVersion version = target.getVersion(); + if (!version.isPreview()) { + max = Math.max(max, version.getApiLevel()); + } + } + } + } + + String[] versions = new String[max]; + for (int api = 1; api <= max; api++) { + versions[api-1] = getAndroidName(api); + } + + return versions; + } + + /** + * Returns the Android project(s) that are selected or active, if any. This + * considers the selection, the active editor, etc. + * + * @param selection the current selection + * @return a list of projects, possibly empty (but never null) + */ + @NonNull + public static List<IProject> getSelectedProjects(@Nullable ISelection selection) { + List<IProject> projects = new ArrayList<IProject>(); + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection) selection; + // get the unique selected item. + Iterator<?> iterator = structuredSelection.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + + // First look up the resource (since some adaptables + // provide an IResource but not an IProject, and we can + // always go from IResource to IProject) + IResource resource = null; + if (element instanceof IResource) { // may include IProject + resource = (IResource) element; + } else if (element instanceof IAdaptable) { + IAdaptable adaptable = (IAdaptable)element; + Object adapter = adaptable.getAdapter(IResource.class); + resource = (IResource) adapter; + } + + // get the project object from it. + IProject project = null; + if (resource != null) { + project = resource.getProject(); + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); + } + + if (project != null && !projects.contains(project)) { + projects.add(project); + } + } + } + + if (projects.isEmpty()) { + // Try to look at the active editor instead + IFile file = AdtUtils.getActiveFile(); + if (file != null) { + projects.add(file.getProject()); + } + } + + if (projects.isEmpty()) { + // If we didn't find a default project based on the selection, check how many + // open Android projects we can find in the current workspace. If there's only + // one, we'll just select it by default. + IJavaProject[] open = AdtUtils.getOpenAndroidProjects(); + for (IJavaProject project : open) { + projects.add(project.getProject()); + } + return projects; + } else { + // Make sure all the projects are Android projects + List<IProject> androidProjects = new ArrayList<IProject>(projects.size()); + for (IProject project : projects) { + if (BaseProjectHelper.isAndroidProject(project)) { + androidProjects.add(project); + } + } + return androidProjects; + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java index 1f45570..4167010 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java @@ -28,6 +28,7 @@ import com.android.assetstudiolib.TabIconGenerator; import com.android.assetstudiolib.TextRenderUtil; import com.android.assetstudiolib.Util; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState.SourceType; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; @@ -505,6 +506,9 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen // Initial image - use the most recently used image, or the default launcher // icon created in our default projects, if there + if (mValues.imagePath != null) { + sImagePath = mValues.imagePath.getPath();; + } if (sImagePath == null) { IProject project = mValues.project; if (project != null) { @@ -544,6 +548,11 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen setSourceType(mValues.sourceType); setShape(mValues.shape); + if (mValues.sourceType == SourceType.CLIPART + && mValues.clipartName != null) { + updateClipartPreview(); + } + // Initial color Display display = mPreviewArea.getDisplay(); //updateColor(display, new RGB(0xa4, 0xc6, 0x39), true /*background*/); @@ -758,27 +767,7 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen mValues.clipartName = (String) image.getData(); close(); - for (Control c : mClipartPreviewPanel.getChildren()) { - c.dispose(); - } - if (mClipartPreviewPanel.getChildren().length == 0) { - try { - BufferedImage icon = GraphicGenerator.getClipartIcon( - mValues.clipartName); - if (icon != null) { - Display display = mClipartForm.getDisplay(); - Image swtImage = SwtUtils.convertToSwt(display, icon, - true, -1); - new ImageControl(mClipartPreviewPanel, - SWT.NONE, swtImage); - } - } catch (IOException e1) { - AdtPlugin.log(e1, null); - } - mClipartPreviewPanel.pack(); - mClipartPreviewPanel.layout(); - } - + updateClipartPreview(); updatePreview(); } } @@ -866,6 +855,29 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen requestUpdatePreview(updateQuickly); } + private void updateClipartPreview() { + for (Control c : mClipartPreviewPanel.getChildren()) { + c.dispose(); + } + if (mClipartPreviewPanel.getChildren().length == 0) { + try { + BufferedImage icon = GraphicGenerator.getClipartIcon( + mValues.clipartName); + if (icon != null) { + Display display = mClipartForm.getDisplay(); + Image swtImage = SwtUtils.convertToSwt(display, icon, + true, -1); + new ImageControl(mClipartPreviewPanel, + SWT.NONE, swtImage); + } + } catch (IOException e1) { + AdtPlugin.log(e1, null); + } + mClipartPreviewPanel.pack(); + mClipartPreviewPanel.layout(); + } + } + private void setShape(GraphicGenerator.Shape shape) { if (shape == GraphicGenerator.Shape.SQUARE) { mSquareRadio.setSelection(true); 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 803478b..396e172 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 @@ -1018,23 +1018,21 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh model.changedModel(); if (AdtPrefs.getPrefs().getFormatGuiXml() && mFormatNode != null) { - if (!mFormatNode.hasError()) { - if (mFormatNode == getUiRootNode()) { - reformatDocument(); - } else { - Node node = mFormatNode.getXmlNode(); - if (node instanceof IndexedRegion) { - IndexedRegion region = (IndexedRegion) node; - int begin = region.getStartOffset(); - int end = region.getEndOffset(); - - if (!mFormatChildren) { - // This will format just the attribute list - end = begin + 1; - } - - reformatRegion(begin, end); + if (mFormatNode == getUiRootNode()) { + reformatDocument(); + } else { + Node node = mFormatNode.getXmlNode(); + if (node instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) node; + int begin = region.getStartOffset(); + int end = region.getEndOffset(); + + if (!mFormatChildren) { + // This will format just the attribute list + end = begin + 1; } + + reformatRegion(begin, end); } } mFormatNode = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java index 6e035af..c63ed18 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java @@ -16,14 +16,15 @@ package com.android.ide.eclipse.adt.internal.editors; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; import static com.android.ide.common.layout.LayoutConstants.ATTR_NAME; import static com.android.ide.common.layout.LayoutConstants.ATTR_ON_CLICK; import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.common.layout.LayoutConstants.VIEW; import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_RESOURCE_REF; +import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_THEME_REF; import static com.android.ide.common.resources.ResourceResolver.PREFIX_RESOURCE_REF; +import static com.android.ide.common.resources.ResourceResolver.PREFIX_THEME_REF; import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG; import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML; import static com.android.ide.eclipse.adt.AdtConstants.FN_RESOURCE_BASE; @@ -38,6 +39,10 @@ import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_NAME; import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_PACKAGE; import static com.android.sdklib.xml.AndroidManifest.NODE_ACTIVITY; import static com.android.sdklib.xml.AndroidManifest.NODE_SERVICE; +import static com.android.tools.lint.detector.api.LintConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.tools.lint.detector.api.LintConstants.NEW_ID_RESOURCE_PREFIX; +import static com.android.tools.lint.detector.api.LintConstants.STYLE_RESOURCE_PREFIX; +import static com.android.util.XmlUtils.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -210,14 +215,14 @@ public class Hyperlinks { } String value = attribute.getValue(); - if (value.startsWith("@+")) { //$NON-NLS-1$ + if (value.startsWith(NEW_ID_RESOURCE_PREFIX)) { // It's a value -declaration-, nowhere else to jump // (though we could consider jumping to the R-file; would that // be helpful?) return false; } - Pair<ResourceType,String> resource = ResourceHelper.parseResource(value); + Pair<ResourceType,String> resource = parseResource(value); if (resource != null) { ResourceType type = resource.getFirst(); if (type != null) { @@ -1026,23 +1031,28 @@ public class Hyperlinks { if (type == ResourceType.ID) { // Ids are recorded in <item> tags instead of <id> tags targetTag = "item"; //$NON-NLS-1$ - } else if (type == ResourceType.ATTR) { + } + + Pair<File, Integer> result = findTag(name, file, parser, document, targetTag); + if (result == null && type == ResourceType.ATTR) { // Attributes seem to be defined in <public> tags targetTag = "public"; //$NON-NLS-1$ + result = findTag(name, file, parser, document, targetTag); } - Element root = document.getDocumentElement(); - if (root.getTagName().equals(ROOT_ELEMENT)) { - NodeList children = root.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element) child; - if (element.getTagName().equals(targetTag)) { - String elementName = element.getAttribute(NAME_ATTR); - if (elementName.equals(name)) { + return result; + } - return Pair.of(file, parser.getOffset(element)); - } + private static Pair<File, Integer> findTag(String name, File file, OffsetTrackingParser parser, + Document document, String targetTag) { + NodeList children = document.getElementsByTagName(targetTag); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) child; + if (element.getTagName().equals(targetTag)) { + String elementName = element.getAttribute(NAME_ATTR); + if (elementName.equals(name)) { + return Pair.of(file, parser.getOffset(element)); } } } @@ -1067,15 +1077,17 @@ public class Hyperlinks { } } - Pair<ResourceType,String> resource = ResourceHelper.parseResource(url); + Pair<ResourceType,String> resource = parseResource(url); if (resource == null) { - String androidStyle = "@android:style/"; //$NON-NLS-1$ + String androidStyle = ANDROID_STYLE_RESOURCE_PREFIX; if (url.startsWith(PREFIX_ANDROID_RESOURCE_REF)) { url = androidStyle + url.substring(PREFIX_ANDROID_RESOURCE_REF.length()); + } else if (url.startsWith(PREFIX_ANDROID_THEME_REF)) { + url = androidStyle + url.substring(PREFIX_ANDROID_THEME_REF.length()); } else if (url.startsWith(ANDROID_PKG + ':')) { url = androidStyle + url.substring(ANDROID_PKG.length() + 1); } else { - url = "@style/" + url; //$NON-NLS-1$ + url = STYLE_RESOURCE_PREFIX + url; } } return getResourceLinks(range, url); @@ -1087,6 +1099,16 @@ public class Hyperlinks { return getResourceLinks(range, url, project, configuration); } + /** Parse a resource reference or a theme reference and return the individual parts */ + private static Pair<ResourceType,String> parseResource(String url) { + if (url.startsWith(PREFIX_THEME_REF)) { + url = PREFIX_RESOURCE_REF + url.substring(PREFIX_THEME_REF.length()); + return ResourceHelper.parseResource(url); + } + + return ResourceHelper.parseResource(url); + } + /** * Computes hyperlinks to resource definitions for resource urls (e.g. * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links. @@ -1101,14 +1123,15 @@ public class Hyperlinks { @NonNull IProject project, @Nullable FolderConfiguration configuration) { List<IHyperlink> links = new ArrayList<IHyperlink>(); - Pair<ResourceType,String> resource = ResourceHelper.parseResource(url); + Pair<ResourceType,String> resource = parseResource(url); if (resource == null || resource.getFirst() == null) { return null; } ResourceType type = resource.getFirst(); String name = resource.getSecond(); - boolean isFramework = url.startsWith("@android"); //$NON-NLS-1$ + boolean isFramework = url.startsWith(PREFIX_ANDROID_RESOURCE_REF) + || url.startsWith(PREFIX_ANDROID_THEME_REF); if (project == null) { // Local reference *within* a framework isFramework = true; @@ -1217,10 +1240,10 @@ public class Hyperlinks { return getStyleLinks(context, range, attribute.getValue()); } if (attribute != null - && attribute.getValue().startsWith(PREFIX_RESOURCE_REF)) { + && (attribute.getValue().startsWith(PREFIX_RESOURCE_REF) + || attribute.getValue().startsWith(PREFIX_THEME_REF))) { // Instantly create links for resources since we can use the existing // resolved maps for this and offer multiple choices for the user - String url = attribute.getValue(); return getResourceLinks(range, url); } @@ -1253,7 +1276,7 @@ public class Hyperlinks { int offset = caretOffset; while (offset > lineStart) { char c = document.getChar(offset); - if (c == '@') { + if (c == '@' || c == '?') { urlStart = offset; break; } else if (!isValidResourceUrlChar(c)) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java index 0a12ba4..4017cf1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/IconFactory.java @@ -20,7 +20,9 @@ package com.android.ide.eclipse.adt.internal.editors; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; import com.android.sdklib.SdkConstants; +import com.google.common.collect.Maps; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; @@ -36,7 +38,8 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.ui.plugin.AbstractUIPlugin; import java.net.URL; -import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; /** * Factory to generate icons for Android Editors. @@ -55,9 +58,11 @@ public class IconFactory { private static IconFactory sInstance; - private HashMap<String, Image> mIconMap = new HashMap<String, Image>(); - private HashMap<URL, Image> mUrlMap = new HashMap<URL, Image>(); - private HashMap<String, ImageDescriptor> mImageDescMap = new HashMap<String, ImageDescriptor>(); + private Map<String, Image> mIconMap = Maps.newHashMap(); + private Map<URL, Image> mUrlMap = Maps.newHashMap(); + private Map<String, ImageDescriptor> mImageDescMap = Maps.newHashMap(); + private Map<Image, Image> mErrorIcons; + private Map<Image, Image> mWarningIcons; private IconFactory() { } @@ -85,6 +90,24 @@ public class IconFactory { } } mUrlMap.clear(); + if (mErrorIcons != null) { + for (Image icon : mErrorIcons.values()) { + // The map can contain null values + if (icon != null) { + icon.dispose(); + } + } + mErrorIcons = null; + } + if (mWarningIcons != null) { + for (Image icon : mWarningIcons.values()) { + // The map can contain null values + if (icon != null) { + icon.dispose(); + } + } + mWarningIcons = null; + } } /** @@ -253,6 +276,56 @@ public class IconFactory { } /** + * Returns an image with an error icon overlaid on it. The icons are cached, + * so the base image should be cached as well, or this method will keep + * storing new overlays into its cache. + * + * @param image the base image + * @return the combined image + */ + @NonNull + public Image addErrorIcon(@NonNull Image image) { + if (mErrorIcons != null) { + Image combined = mErrorIcons.get(image); + if (combined != null) { + return combined; + } + } else { + mErrorIcons = new IdentityHashMap<Image, Image>(); + } + + Image combined = new ErrorImageComposite(image, false).createImage(); + mErrorIcons.put(image, combined); + + return combined; + } + + /** + * Returns an image with a warning icon overlaid on it. The icons are + * cached, so the base image should be cached as well, or this method will + * keep storing new overlays into its cache. + * + * @param image the base image + * @return the combined image + */ + @NonNull + public Image addWarningIcon(@NonNull Image image) { + if (mWarningIcons != null) { + Image combined = mWarningIcons.get(image); + if (combined != null) { + return combined; + } + } else { + mWarningIcons = new IdentityHashMap<Image, Image>(); + } + + Image combined = new ErrorImageComposite(image, true).createImage(); + mWarningIcons.put(image, combined); + + return combined; + } + + /** * A simple image description that generates a 16x16 image which consists * of a colored letter inside a black & white circle. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java index 613a68f..7cfe791 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ElementDescriptor.java @@ -469,4 +469,17 @@ public class ElementDescriptor implements Comparable<ElementDescriptor> { public int compareTo(ElementDescriptor o) { return mUiName.compareToIgnoreCase(o.mUiName); } + + /** + * Ensures that this view descriptor's attribute list is up to date. This is + * always the case for all the builtin descriptors, but for example for a + * custom view, it could be changing dynamically so caches may have to be + * recomputed. This method will return true if nothing changed, and false if + * it recomputed its info. + * + * @return true if the attributes are already up to date and nothing changed + */ + public boolean syncAttributes() { + return true; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java index 4bc7641..89dd263 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java @@ -39,13 +39,18 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; +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.lint.EclipseLintClient; +import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; +import com.android.tools.lint.client.api.IssueRegistry; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -78,8 +83,11 @@ import org.eclipse.wst.sse.ui.StructuredTextEditor; import org.w3c.dom.Document; import org.w3c.dom.Node; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -138,6 +146,7 @@ public class LayoutEditorDelegate extends CommonXmlDelegate private final HashMap<String, ElementDescriptor> mUnknownDescriptorMap = new HashMap<String, ElementDescriptor>(); + private EclipseLintClient mClient; /** * Flag indicating if the replacement file is due to a config change. @@ -405,27 +414,106 @@ public class LayoutEditorDelegate extends CommonXmlDelegate return true; } + /** + * Returns one of the issues for the given node (there could be more than one) + * + * @param node the node to look up lint issues for + * @return the marker for one of the issues found for the given node + */ + @Nullable + public IMarker getIssueForNode(@Nullable UiViewElementNode node) { + if (node == null) { + return null; + } + + if (mClient != null) { + return mClient.getIssueForNode(node); + } + + return null; + } + + /** + * Returns a collection of nodes that have one or more lint warnings + * associated with them (retrievable via + * {@link #getIssueForNode(UiViewElementNode)}) + * + * @return a collection of nodes, which should <b>not</b> be modified by the + * caller + */ + @Nullable + public Collection<Node> getLintNodes() { + if (mClient != null) { + return mClient.getIssueNodes(); + } + + return null; + } + @Override public Job delegateRunLint() { - Job job = super.delegateRunLint(); + // We want to customize the {@link EclipseLintClient} created to run this + // single file lint, in particular such that we can set the mode which collects + // nodes on that lint job, such that we can quickly look up error nodes + //Job job = super.delegateRunLint(); + + Job job = null; + IFile file = getEditor().getInputFile(); + if (file != null) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + List<IFile> resources = Collections.singletonList(file); + mClient = new EclipseLintClient(registry, + resources, getEditor().getStructuredDocument(), false /*fatal*/); + + mClient.setCollectNodes(true); + + job = EclipseLintRunner.startLint(mClient, resources, file, + false /*show*/); + } if (job != null) { - job.addJobChangeListener(new JobChangeAdapter() { - @Override - public void done(IJobChangeEvent event) { - GraphicalEditorPart graphicalEditor = getGraphicalEditor(); - if (graphicalEditor != null) { - LayoutActionBar bar = graphicalEditor.getLayoutActionBar(); - if (!bar.isDisposed()) { - bar.updateErrorIndicator(); - } - } - } - }); + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); + if (graphicalEditor != null) { + job.addJobChangeListener(new LintJobListener(graphicalEditor)); + } } return job; } + private class LintJobListener extends JobChangeAdapter implements Runnable { + private final GraphicalEditorPart mEditor; + private final LayoutCanvas mCanvas; + + LintJobListener(GraphicalEditorPart editor) { + mEditor = editor; + mCanvas = editor.getCanvasControl(); + } + + @Override + public void done(IJobChangeEvent event) { + LayoutActionBar bar = mEditor.getLayoutActionBar(); + if (!bar.isDisposed()) { + bar.updateErrorIndicator(); + } + + // Redraw + if (!mCanvas.isDisposed()) { + mCanvas.getDisplay().asyncExec(this); + } + } + + @Override + public void run() { + if (!mCanvas.isDisposed()) { + mCanvas.redraw(); + + OutlinePage outlinePage = mCanvas.getOutlinePage(); + if (outlinePage != null) { + outlinePage.refreshIcons(); + } + } + } + } /** * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java index 1fa3f06..5330752 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationComposite.java @@ -1020,7 +1020,11 @@ public class ConfigurationComposite extends Composite implements SelectionListen } private IAndroidTarget getSelectedTarget() { - return (IAndroidTarget) mTargetCombo.getData(); + if (!mTargetCombo.isDisposed()) { + return (IAndroidTarget) mTargetCombo.getData(); + } + + return null; } void selectTheme(String theme) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java index 194cb3f..7e7d65b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java @@ -188,7 +188,7 @@ class ThemeMenuAction extends SubmenuAction { } } - String manifestTheme = manifest.getmManifestTheme(); + String manifestTheme = manifest.getManifestTheme(); if (activityThemes.size() > 0 || manifestTheme != null) { Set<String> allThemes = new HashSet<String>(activityThemes.values()); if (manifestTheme != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java index a110c65..ab4e1e9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java @@ -16,11 +16,11 @@ package com.android.ide.eclipse.adt.internal.editors.layout.descriptors; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.sdklib.SdkConstants.CLASS_VIEWGROUP; import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI; import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX; import static com.android.util.XmlUtils.ANDROID_NS_NAME_PREFIX; +import static com.android.util.XmlUtils.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -44,12 +44,14 @@ import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceType; import com.android.sdklib.IAndroidTarget; +import com.google.common.collect.Maps; import com.google.common.collect.ObjectArrays; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IClassFile; @@ -193,7 +195,8 @@ public final class CustomViewDescriptorService { // we have a valid parent, lets create a new ViewElementDescriptor. List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>(); List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>(); - findCustomDescriptors(project, type, attrList, paramList); + Map<ResourceFile, Long> files = findCustomDescriptors(project, type, + attrList, paramList); AttributeDescriptor[] attributes = getAttributeDescriptor(type, parentDescriptor); @@ -209,7 +212,8 @@ public final class CustomViewDescriptorService { ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn, attributes, layoutAttributes, - parentDescriptor.getChildren()); + parentDescriptor.getChildren(), + project, files); descriptor.setSuperClass(parentDescriptor); synchronized (mCustomDescriptorMap) { @@ -270,7 +274,7 @@ public final class CustomViewDescriptorService { } /** Compute/find the styleable resources for the given type, if possible */ - private void findCustomDescriptors( + private Map<ResourceFile, Long> findCustomDescriptors( IProject project, IType type, List<AttributeDescriptor> customAttributes, @@ -286,6 +290,8 @@ public final class CustomViewDescriptorService { Set<ResourceFile> resourceFiles = findAttrsFiles(library, className); if (resourceFiles != null && resourceFiles.size() > 0) { String appUri = getAppResUri(project); + Map<ResourceFile, Long> timestamps = + Maps.newHashMapWithExpectedSize(resourceFiles.size()); for (ResourceFile file : resourceFiles) { AttrsXmlParser attrsXmlParser = getParser(file); String fqcn = type.getFullyQualifiedName(); @@ -300,8 +306,14 @@ public final class CustomViewDescriptorService { classInfo, "Layout", null /*superClassInfo*/); //$NON-NLS-1$ attrsXmlParser.loadLayoutParamsAttributes(layoutInfo); appendAttributes(customLayoutAttributes, layoutInfo.getAttributes(), appUri); + + timestamps.put(file, file.getFile().getModificationStamp()); } + + return timestamps; } + + return null; } /** @@ -309,7 +321,7 @@ public final class CustomViewDescriptorService { * attributes for the given class name */ @Nullable - private Set<ResourceFile> findAttrsFiles(IProject library, String className) { + private static Set<ResourceFile> findAttrsFiles(IProject library, String className) { Set<ResourceFile> resourceFiles = null; ResourceManager manager = ResourceManager.getInstance(); ProjectResources resources = manager.getProjectResources(library); @@ -338,7 +350,7 @@ public final class CustomViewDescriptorService { * attrs.xml file in the same project. */ @Nullable - private IProject getProjectDeclaringType(IType type) { + private static IProject getProjectDeclaringType(IType type) { IClassFile classFile = type.getClassFile(); if (classFile != null) { IPath path = classFile.getPath(); @@ -358,7 +370,7 @@ public final class CustomViewDescriptorService { } /** Returns the name space to use for application attributes */ - private String getAppResUri(IProject project) { + private static String getAppResUri(IProject project) { String appResource; ProjectState projectState = Sdk.getProjectState(project); if (projectState != null && projectState.isLibrary()) { @@ -459,7 +471,7 @@ public final class CustomViewDescriptorService { ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn, getAttributeDescriptor(type, parentDescriptor), getLayoutAttributeDescriptors(type, parentDescriptor), - children); + children, project, null); descriptor.setSuperClass(parentDescriptor); // add it to the map @@ -504,10 +516,14 @@ public final class CustomViewDescriptorService { return parentDescriptor.getLayoutAttributes(); } - private static class CustomViewDescriptor extends ViewElementDescriptor { + private class CustomViewDescriptor extends ViewElementDescriptor { + private Map<ResourceFile, Long> mTimeStamps; + private IProject mProject; + public CustomViewDescriptor(String name, String fqcn, AttributeDescriptor[] attributes, AttributeDescriptor[] layoutAttributes, - ElementDescriptor[] children) { + ElementDescriptor[] children, IProject project, + Map<ResourceFile, Long> timestamps) { super( fqcn, // xml name name, // ui name @@ -519,6 +535,8 @@ public final class CustomViewDescriptorService { children, false // mandatory ); + mTimeStamps = timestamps; + mProject = project; } @Override @@ -533,5 +551,71 @@ public final class CustomViewDescriptorService { return iconFactory.getIcon("customView"); //$NON-NLS-1$ } + + @Override + public boolean syncAttributes() { + // Check if any of the descriptors + if (mTimeStamps != null) { + // Prevent checking actual file timestamps too frequently on rapid burst calls + long now = System.currentTimeMillis(); + if (now - sLastCheck < 1000) { + return true; + } + sLastCheck = now; + + // Check whether the resource files (typically just one) which defined + // custom attributes for this custom view have changed, and if so, + // refresh the attribute descriptors. + // This doesn't work the cases where you add descriptors for a custom + // view after using it, or add attributes in a separate file, but those + // scenarios aren't quite as common (and would require a bit more expensive + // analysis.) + for (Map.Entry<ResourceFile, Long> entry : mTimeStamps.entrySet()) { + ResourceFile file = entry.getKey(); + Long timestamp = entry.getValue(); + boolean recompute = false; + if (file.getFile().getModificationStamp() > timestamp.longValue()) { + // One or more attributes changed: recompute + recompute = true; + mParserCache.remove(file); + } + + if (recompute) { + IJavaProject javaProject = JavaCore.create(mProject); + String fqcn = getFullClassName(); + IType type = null; + try { + type = javaProject.findType(fqcn); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + if (type == null || !type.exists()) { + return true; + } + + List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>(); + List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>(); + + mTimeStamps = findCustomDescriptors(mProject, type, attrList, paramList); + + ViewElementDescriptor parentDescriptor = getSuperClassDesc(); + AttributeDescriptor[] attributes = + getAttributeDescriptor(type, parentDescriptor); + if (!attrList.isEmpty()) { + attributes = join(attrList, attributes); + } + attributes = attrList.toArray(new AttributeDescriptor[attrList.size()]); + setAttributes(attributes); + + return false; + } + } + } + + return true; + } } + + /** Timestamp of the most recent {@link CustomViewDescriptor#syncAttributes} check */ + private static long sLastCheck; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java index a515640..b3dce07 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.custom.ScrolledComposite; @@ -28,7 +29,6 @@ import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; -import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; @@ -218,10 +218,7 @@ public abstract class AccordionControl extends Composite { label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$ updateBackground(label, false); if (labelFont == null) { - labelFont = label.getFont(); - FontData normal = labelFont.getFontData()[0]; - FontData bold = new FontData(normal.getName(), normal.getHeight(), SWT.BOLD); - labelFont = new Font(null, bold); + labelFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT); } label.setFont(labelFont); label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java index 0d7fc28..7cbb8f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java @@ -179,4 +179,8 @@ public class CanvasTransform { public int inverseTranslate(int screenX) { return (int) ((screenX - mMargin + mTranslate) / mScale); } + + public int inverseScale(int canwasW) { + return (int) (canwasW / mScale); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index d4f25f8..493f996 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -17,7 +17,6 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.common.layout.LayoutConstants.ANDROID_STRING_PREFIX; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; @@ -30,6 +29,7 @@ import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; import static com.android.sdklib.SdkConstants.FD_GEN_SOURCES; +import static com.android.util.XmlUtils.ANDROID_URI; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_COLLAPSED; @@ -74,7 +74,6 @@ import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFa import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; -import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; @@ -273,6 +272,7 @@ public class GraphicalEditorPart extends EditorPart private FlyoutControlComposite mStructureFlyout; private FlyoutControlComposite mPaletteComposite; private PropertyFactory mPropertyFactory; + private boolean mRenderedOnce; /** * Flags which tracks whether this editor is currently active which is set whenever @@ -400,7 +400,7 @@ public class GraphicalEditorPart extends EditorPart GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); mActionBar.setLayoutData(detailsData); if (file != null) { - mActionBar.updateErrorIndicator(EclipseLintClient.hasMarkers(file)); + mActionBar.updateErrorIndicator(file); } mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER); @@ -1611,6 +1611,21 @@ public class GraphicalEditorPart extends EditorPart } else { // Nope, no missing or broken classes. Clear success, congrats! hideError(); + + // First time this layout is opened, run lint on the file (after a delay) + if (!mRenderedOnce) { + mRenderedOnce = true; + Job job = new Job("Run Lint") { + @Override + protected IStatus run(IProgressMonitor monitor) { + getEditorDelegate().delegateRunLint(); + return Status.OK_STATUS; + } + + }; + job.setSystem(true); + job.schedule(3000); // 3 seconds + } } model.refreshUi(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java index 23ddaa1..53dd881 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java @@ -16,6 +16,9 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.events.MouseEvent; @@ -60,7 +63,7 @@ public class ImageControl extends Canvas implements MouseTrackListener { * the control is disposed (unless the {@link #setDisposeImage} method is * called to turn off auto dispose) */ - public ImageControl(Composite parent, int style, Image image) { + public ImageControl(@NonNull Composite parent, int style, @Nullable Image image) { super(parent, style | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED); this.mImage = image; @@ -72,11 +75,32 @@ public class ImageControl extends Canvas implements MouseTrackListener { }); } - public void setImage(Image image) { - if (mDisposeImage) { + @Nullable + public Image getImage() { + return mImage; + } + + public void setImage(@Nullable Image image) { + if (mDisposeImage && mImage != null) { mImage.dispose(); } mImage = image; + redraw(); + } + + public void fitToWidth(int width) { + if (mImage == null) { + return; + } + Rectangle imageRect = mImage.getBounds(); + int imageWidth = imageRect.width; + if (imageWidth <= width) { + mScale = 1.0f; + return; + } + + mScale = width / (float) imageWidth; + redraw(); } public void setScale(float scale) { @@ -87,13 +111,17 @@ public class ImageControl extends Canvas implements MouseTrackListener { return mScale; } - public void setHoverColor(Color hoverColor) { + public void setHoverColor(@Nullable Color hoverColor) { + if (mHoverColor != null) { + removeMouseTrackListener(this); + } mHoverColor = hoverColor; if (hoverColor != null) { addMouseTrackListener(this); } } + @Nullable public Color getHoverColor() { return mHoverColor; } @@ -102,7 +130,7 @@ public class ImageControl extends Canvas implements MouseTrackListener { public void dispose() { super.dispose(); - if (mDisposeImage) { + if (mDisposeImage && mImage != null && !mImage.isDisposed()) { mImage.dispose(); } mImage = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java index a5df4bd..c402b2b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java @@ -32,6 +32,7 @@ import org.eclipse.swt.graphics.PaletteData; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.awt.image.WritableRaster; +import java.lang.ref.SoftReference; /** * The {@link ImageOverlay} class renders an image as an overlay. @@ -56,7 +57,17 @@ public class ImageOverlay extends Overlay implements IImageFactory { /** Current background AWT image. This is created by {@link #getImage()}, which is called * by the LayoutLib. */ - private BufferedImage mAwtImage; + private SoftReference<BufferedImage> mAwtImage = new SoftReference<BufferedImage>(null); + + /** + * Strong reference to the image in the above soft reference, to prevent + * garbage collection when {@link PRESCALE} is set, until the scaled image + * is created (lazily as part of the next paint call, where this strong + * reference is nulled out and the above soft reference becomes eligible to + * be reclaimed when memory is low.) + */ + @SuppressWarnings("unused") // Used by the garbage collector to keep mAwtImage non-soft + private BufferedImage mAwtImageStrongRef; /** The associated {@link LayoutCanvas}. */ private LayoutCanvas mCanvas; @@ -109,8 +120,10 @@ public class ImageOverlay extends Overlay implements IImageFactory { * @return The corresponding SWT image, or null. */ public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) { - if (awtImage != mAwtImage || awtImage == null) { - mAwtImage = null; + BufferedImage oldAwtImage = mAwtImage.get(); + if (awtImage != oldAwtImage || awtImage == null) { + mAwtImage.clear(); + mAwtImageStrongRef = null; if (mImage != null) { mImage.dispose(); @@ -165,7 +178,12 @@ public class ImageOverlay extends Overlay implements IImageFactory { */ @Nullable BufferedImage getAwtImage() { - return mAwtImage; + BufferedImage awtImage = mAwtImage.get(); + if (awtImage == null && mImage != null) { + awtImage = SwtUtils.convertToAwt(mImage); + } + + return awtImage; } @Override @@ -184,11 +202,12 @@ public class ImageOverlay extends Overlay implements IImageFactory { // This is done lazily in paint rather than when the image changes because // the image must be rescaled each time the zoom level changes, which varies // independently from when the image changes. - if (PRESCALE && mAwtImage != null) { + BufferedImage awtImage = mAwtImage.get(); + if (PRESCALE && awtImage != null) { if (mPreScaledImage == null || mPreScaledImage.getImageData().width != hi.getScalledImgSize()) { - double xScale = hi.getScalledImgSize() / (double) mAwtImage.getWidth(); - double yScale = vi.getScalledImgSize() / (double) mAwtImage.getHeight(); + double xScale = hi.getScalledImgSize() / (double) awtImage.getWidth(); + double yScale = vi.getScalledImgSize() / (double) awtImage.getHeight(); BufferedImage scaledAwtImage; // NOTE: == comparison on floating point numbers is okay @@ -198,9 +217,9 @@ public class ImageOverlay extends Overlay implements IImageFactory { // of rounding errors. if (xScale == 1.0 && yScale == 1.0) { // Scaling to 100% is easy! - scaledAwtImage = mAwtImage; + scaledAwtImage = awtImage; } else { - scaledAwtImage = ImageUtils.scale(mAwtImage, xScale, yScale); + scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale); } assert scaledAwtImage.getWidth() == hi.getScalledImgSize(); if (mPreScaledImage != null && !mPreScaledImage.isDisposed()) { @@ -208,6 +227,8 @@ public class ImageOverlay extends Overlay implements IImageFactory { } mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage, true /*transferAlpha*/, -1); + // We can't just clear the mAwtImageStrongRef here, because if the + // zooming factor changes, we may need to use it again } if (mPreScaledImage != null) { @@ -349,14 +370,19 @@ public class ImageOverlay extends Overlay implements IImageFactory { */ @Override public BufferedImage getImage(int w, int h) { - if (mAwtImage == null || - mAwtImage.getWidth() != w || - mAwtImage.getHeight() != h) { - - mAwtImage = SwtReadyBufferedImage.createImage(w, h, getDevice()); + BufferedImage awtImage = mAwtImage.get(); + if (awtImage == null || + awtImage.getWidth() != w || + awtImage.getHeight() != h) { + mAwtImage.clear(); + awtImage = SwtReadyBufferedImage.createImage(w, h, getDevice()); + mAwtImage = new SoftReference<BufferedImage>(awtImage); + if (PRESCALE) { + mAwtImageStrongRef = awtImage; + } } - return mAwtImage; + return awtImage; } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java index 1c83430..63a36f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java @@ -15,8 +15,8 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import static com.android.util.XmlUtils.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; +import static com.android.util.XmlUtils.ANDROID_URI; import com.android.annotations.NonNull; import com.android.ide.common.api.INode; @@ -26,6 +26,7 @@ import com.android.ide.common.api.RuleAction.Separator; import com.android.ide.common.api.RuleAction.Toggle; import com.android.ide.common.layout.BaseViewRule; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; @@ -35,6 +36,7 @@ import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; import com.google.common.base.Strings; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; @@ -553,9 +555,10 @@ public class LayoutActionBar extends Composite { mLintButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - IFile file = mEditor.getEditorDelegate().getEditor().getInputFile(); + CommonXmlEditor editor = mEditor.getEditorDelegate().getEditor(); + IFile file = editor.getInputFile(); if (file != null) { - EclipseLintClient.showErrors(getShell(), file); + EclipseLintClient.showErrors(getShell(), file, editor); } } }); @@ -567,7 +570,17 @@ public class LayoutActionBar extends Composite { * Updates the lint indicator state in the given layout editor */ public void updateErrorIndicator() { - updateErrorIndicator(EclipseLintClient.hasMarkers(mEditor.getEditedFile())); + updateErrorIndicator(mEditor.getEditedFile()); + } + + /** + * Updates the lint indicator state for the given file + * + * @param file the file to show the indicator status for + */ + public void updateErrorIndicator(IFile file) { + IMarker[] markers = EclipseLintClient.getMarkers(file); + updateErrorIndicator(markers.length); } /** @@ -575,14 +588,14 @@ public class LayoutActionBar extends Composite { * * @param hasLintWarnings whether there are lint errors to be shown */ - void updateErrorIndicator(final boolean hasLintWarnings) { + private void updateErrorIndicator(final int markerCount) { Display display = getDisplay(); if (display.getThread() != Thread.currentThread()) { display.asyncExec(new Runnable() { @Override public void run() { if (!isDisposed()) { - updateErrorIndicator(hasLintWarnings); + updateErrorIndicator(markerCount); } } }); @@ -590,10 +603,37 @@ public class LayoutActionBar extends Composite { } GridData layoutData = (GridData) mLintToolBar.getLayoutData(); - if (layoutData.exclude == hasLintWarnings) { - layoutData.exclude = !hasLintWarnings; - mLintToolBar.setVisible(hasLintWarnings); - layout(); + Integer existing = (Integer) mLintToolBar.getData(); + Integer current = Integer.valueOf(markerCount); + if (!current.equals(existing)) { + mLintToolBar.setData(current); + boolean layout = false; + boolean hasLintWarnings = markerCount > 0; + if (layoutData.exclude == hasLintWarnings) { + layoutData.exclude = !hasLintWarnings; + mLintToolBar.setVisible(hasLintWarnings); + layout = true; + } + if (markerCount > 0) { + String iconName = ""; + switch (markerCount) { + case 1: iconName = "lint1"; break; //$NON-NLS-1$ + case 2: iconName = "lint2"; break; //$NON-NLS-1$ + case 3: iconName = "lint3"; break; //$NON-NLS-1$ + case 4: iconName = "lint4"; break; //$NON-NLS-1$ + case 5: iconName = "lint5"; break; //$NON-NLS-1$ + case 6: iconName = "lint6"; break; //$NON-NLS-1$ + case 7: iconName = "lint7"; break; //$NON-NLS-1$ + case 8: iconName = "lint8"; break; //$NON-NLS-1$ + case 9: iconName = "lint9"; break; //$NON-NLS-1$ + default: iconName = "lint9p"; break;//$NON-NLS-1$ + } + mLintButton.setImage(IconFactory.getInstance().getIcon(iconName)); + } + if (layout) { + layout(); + } + redraw(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 481c215..2154179 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -197,6 +197,9 @@ public class LayoutCanvas extends Canvas { /** The overlay which paints the mouse hover. */ private HoverOverlay mHoverOverlay; + /** The overlay which paints the lint warnings */ + private LintOverlay mLintOverlay; + /** The overlay which paints the selection. */ private SelectionOverlay mSelectionOverlay; @@ -267,6 +270,8 @@ public class LayoutCanvas extends Canvas { mImageOverlay = new ImageOverlay(this, mHScale, mVScale); mIncludeOverlay = new IncludeOverlay(this); mImageOverlay.create(display); + mLintOverlay = new LintOverlay(this); + mLintOverlay.create(display); // --- Set up listeners addPaintListener(new PaintListener() { @@ -310,6 +315,8 @@ public class LayoutCanvas extends Canvas { if (editorDelegate != null) { mOutlinePage = editorDelegate.getGraphicalOutline(); } + + new LintTooltipManager(this).register(); } private Runnable mZoomCheck = new Runnable() { @@ -423,6 +430,11 @@ public class LayoutCanvas extends Canvas { mIncludeOverlay = null; } + if (mLintOverlay != null) { + mLintOverlay.dispose(); + mLintOverlay = null; + } + mViewHierarchy.dispose(); } @@ -651,7 +663,11 @@ public class LayoutCanvas extends Canvas { * rendered image, but it will never zoom in. */ void setFitScale(boolean onlyZoomOut) { - Image image = getImageOverlay().getImage(); + ImageOverlay imageOverlay = getImageOverlay(); + if (imageOverlay == null) { + return; + } + Image image = imageOverlay.getImage(); if (image != null) { Rectangle canvasSize = getClientArea(); int canvasWidth = canvasSize.width; @@ -774,6 +790,11 @@ public class LayoutCanvas extends Canvas { if (!mHoverOverlay.isHiding()) { mHoverOverlay.paint(gc); } + + if (!mLintOverlay.isHiding()) { + mLintOverlay.paint(gc); + } + if (!mIncludeOverlay.isHiding()) { mIncludeOverlay.paint(gc); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java new file mode 100644 index 0000000..d5d5ce7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.editors.layout.gle2; + +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.Node; + +import java.util.Collection; + +import lombok.ast.libs.org.parboiled.google.collect.Lists; + +/** + * The {@link LintOverlay} paints an icon over each view that contains at least one + * lint error (unless the view is smaller than the icon) + */ +public class LintOverlay extends Overlay { + /** Approximate size of lint overlay icons */ + static final int ICON_SIZE = 8; + /** Alpha to draw lint overlay icons with */ + private static final int ALPHA = 192; + + private final LayoutCanvas mCanvas; + private Image mWarningImage; + private Image mErrorImage; + + /** + * Constructs a new {@link LintOverlay} + * + * @param canvas the associated canvas + */ + public LintOverlay(LayoutCanvas canvas) { + mCanvas = canvas; + } + + @Override + public void paint(GC gc) { + LayoutEditorDelegate editor = mCanvas.getEditorDelegate(); + Collection<Node> nodes = editor.getLintNodes(); + if (nodes != null && !nodes.isEmpty()) { + // Copy list before iterating through it to avoid a concurrent list modification + // in case lint runs in the background while painting and updates this list + nodes = Lists.newArrayList(nodes); + ViewHierarchy hierarchy = mCanvas.getViewHierarchy(); + Image icon = getWarningIcon(); + ImageData imageData = icon.getImageData(); + int iconWidth = imageData.width; + int iconHeight = imageData.height; + CanvasTransform mHScale = mCanvas.getHorizontalTransform(); + CanvasTransform mVScale = mCanvas.getVerticalTransform(); + + int oldAlpha = gc.getAlpha(); + try { + gc.setAlpha(ALPHA); + for (Node node : nodes) { + CanvasViewInfo vi = hierarchy.findViewInfoFor(node); + if (vi != null) { + Rectangle bounds = vi.getAbsRect(); + int x = mHScale.translate(bounds.x); + int y = mVScale.translate(bounds.y); + int w = mHScale.scale(bounds.width); + int h = mVScale.scale(bounds.height); + if (w < iconWidth || h < iconHeight) { + // Don't draw badges on tiny widgets (including those + // that aren't tiny but are zoomed out too far) + continue; + } + + x += w - iconWidth; + y += h - iconHeight; + + boolean isError = false; + IMarker marker = editor.getIssueForNode(vi.getUiViewNode()); + if (marker != null) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + isError = severity == IMarker.SEVERITY_ERROR; + } + + icon = isError ? getErrorIcon() : getWarningIcon(); + + gc.drawImage(icon, x, y); + } + } + } finally { + gc.setAlpha(oldAlpha); + } + } + } + + private Image getWarningIcon() { + if (mWarningImage == null) { + mWarningImage = IconFactory.getInstance().getIcon("warning-badge"); //$NON-NLS-1$ + } + + return mWarningImage; + } + + private Image getErrorIcon() { + if (mErrorImage == null) { + mErrorImage = IconFactory.getInstance().getIcon("error-badge"); //$NON-NLS-1$ + } + + return mErrorImage; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java new file mode 100644 index 0000000..bc2f390 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.gle2; + +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; + +import com.android.ide.common.layout.BaseLayoutRule; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.util.List; + +/** Actual tooltip showing multiple lines for various widgets that have lint errors */ +class LintTooltip extends Shell { + private final LayoutCanvas mCanvas; + private final List<UiViewElementNode> mNodes; + + LintTooltip(LayoutCanvas canvas, List<UiViewElementNode> nodes) { + super(canvas.getDisplay(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + mCanvas = canvas; + mNodes = nodes; + + createContents(); + } + + protected void createContents() { + Display display = getDisplay(); + Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + setBackground(bg); + GridLayout gridLayout = new GridLayout(2, false); + setLayout(gridLayout); + + LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); + + boolean first = true; + for (UiViewElementNode node : mNodes) { + IMarker marker = delegate.getIssueForNode(node); + if (marker != null) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Label icon = new Label(this, SWT.NONE); + icon.setForeground(fg); + icon.setBackground(bg); + icon.setImage(node.getIcon()); + + Label label = new Label(this, SWT.WRAP); + if (first) { + label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1)); + first = false; + } + + String id = BaseLayoutRule.stripIdPrefix(node.getAttributeValue(ATTR_ID)); + if (id.isEmpty()) { + if (node.getXmlNode() != null) { + id = node.getXmlNode().getNodeName(); + } else { + id = node.getDescriptor().getUiName(); + } + } + + label.setText(String.format("%1$s: %2$s", id, message)); + } + } + } + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java new file mode 100644 index 0000000..a2bbdf4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.editors.layout.gle2; + +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LintOverlay.ICON_SIZE; + +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** Tooltip in the layout editor showing lint errors under the cursor */ +class LintTooltipManager implements Listener { + private final LayoutCanvas mCanvas; + private Shell mTip = null; + private List<UiViewElementNode> mShowingNodes; + + /** + * Sets up a custom tooltip when hovering over tree items. It currently displays the error + * message for the lint warning associated with each node, if any (and only if the hover + * is over the icon portion). + */ + LintTooltipManager(LayoutCanvas canvas) { + mCanvas = canvas; + } + + void register() { + mCanvas.addListener(SWT.Dispose, this); + mCanvas.addListener(SWT.KeyDown, this); + mCanvas.addListener(SWT.MouseMove, this); + mCanvas.addListener(SWT.MouseHover, this); + } + + void unregister() { + mCanvas.removeListener(SWT.Dispose, this); + mCanvas.removeListener(SWT.KeyDown, this); + mCanvas.removeListener(SWT.MouseMove, this); + mCanvas.removeListener(SWT.MouseHover, this); + } + + @Override + public void handleEvent(Event event) { + switch(event.type) { + case SWT.MouseMove: + // See if we're still overlapping this or *other* errors; if so, keep the + // tip up (or update it). + if (mShowingNodes != null) { + List<UiViewElementNode> nodes = computeNodes(event); + if (nodes != null && !nodes.isEmpty()) { + if (nodes.equals(mShowingNodes)) { + return; + } else { + show(nodes); + } + break; + } + } + + // If not, fall through and hide the tooltip + + //$FALL-THROUGH$ + case SWT.Dispose: + case SWT.FocusOut: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + hide(); + break; + case SWT.MouseHover: + hide(); + show(event); + break; + } + } + + private void hide() { + if (mTip != null) { + mTip.dispose(); + mTip = null; + } + mShowingNodes = null; + } + + private void show(Event event) { + List<UiViewElementNode> nodes = computeNodes(event); + if (nodes != null && !nodes.isEmpty()) { + show(nodes); + } + } + + /** Show a tooltip listing the lint errors for the given nodes */ + private void show(List<UiViewElementNode> nodes) { + hide(); + + mTip = new LintTooltip(mCanvas, nodes); + Rectangle rect = mCanvas.getBounds(); + Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + Point pos = mCanvas.toDisplay(rect.x, rect.y + rect.height); + if (size.x > rect.width) { + size = mTip.computeSize(rect.width, SWT.DEFAULT); + } + mTip.setBounds(pos.x, pos.y, size.x, size.y); + + mShowingNodes = nodes; + mTip.setVisible(true); + } + + /** + * Compute the list of nodes which have lint warnings near the given mouse + * coordinates + * + * @param event the mouse cursor event + * @return a list of nodes, possibly empty + */ + @Nullable + private List<UiViewElementNode> computeNodes(Event event) { + LayoutPoint p = ControlPoint.create(mCanvas, event.x, event.y).toLayout(); + LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); + ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy(); + CanvasTransform mHScale = mCanvas.getHorizontalTransform(); + CanvasTransform mVScale = mCanvas.getVerticalTransform(); + + int layoutIconSize = mHScale.inverseScale(ICON_SIZE); + int slop = mVScale.inverseScale(10); // extra space around icon where tip triggers + + Collection<Node> xmlNodes = delegate.getLintNodes(); + if (xmlNodes == null) { + return null; + } + List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(); + for (Node xmlNode : xmlNodes) { + CanvasViewInfo v = viewHierarchy.findViewInfoFor(xmlNode); + if (v != null) { + Rectangle b = v.getAbsRect(); + int x2 = b.x + b.width; + int y2 = b.y + b.height; + if (p.x < x2 - layoutIconSize - slop + || p.x > x2 + slop + || p.y < y2 - layoutIconSize - slop + || p.y > y2 + slop) { + continue; + } + + nodes.add(v.getUiViewNode()); + } + } + + return nodes; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java index eca47ab..9261aff 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java @@ -16,25 +16,20 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import static com.android.util.XmlUtils.ANDROID_URI; -import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; import static com.android.ide.common.layout.LayoutConstants.ATTR_COLUMN_COUNT; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; -import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.ATTR_ROW_COUNT; import static com.android.ide.common.layout.LayoutConstants.ATTR_SRC; import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; import static com.android.ide.common.layout.LayoutConstants.DRAWABLE_PREFIX; import static com.android.ide.common.layout.LayoutConstants.GRID_LAYOUT; import static com.android.ide.common.layout.LayoutConstants.LAYOUT_PREFIX; -import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; -import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; -import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG; import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI; import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX; +import static com.android.util.XmlUtils.ANDROID_URI; import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER; import com.android.annotations.VisibleForTesting; @@ -45,7 +40,6 @@ import com.android.ide.common.layout.GridLayoutRule; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; -import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; @@ -53,12 +47,12 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; -import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.util.Pair; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; @@ -94,9 +88,18 @@ import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MenuDetectListener; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.IActionBars; @@ -256,6 +259,17 @@ public class OutlinePage extends ContentOutlinePage } } + /** Refresh all the icon state */ + public void refreshIcons() { + TreeViewer treeViewer = getTreeViewer(); + if (treeViewer != null) { + Tree tree = treeViewer.getTree(); + if (tree != null && !tree.isDisposed()) { + treeViewer.refresh(); + } + } + } + /** * Set whether the outline should be shown in the header * @@ -442,6 +456,8 @@ public class OutlinePage extends ContentOutlinePage public void keyReleased(KeyEvent e) { } }); + + setupTooltip(); } private void createPropertySheet() { @@ -686,45 +702,9 @@ public class OutlinePage extends ContentOutlinePage element = ((CanvasViewInfo) element).getUiViewNode(); } - if (element instanceof UiElementNode) { - UiElementNode node = (UiElementNode) element; - ElementDescriptor desc = node.getDescriptor(); - if (desc != null) { - Image img = null; - // Special case for the common case of vertical linear layouts: - // show vertical linear icon (the default icon shows horizontal orientation) - String uiName = desc.getUiName(); - if (uiName.equals(LINEAR_LAYOUT)) { - Element e = (Element) node.getXmlNode(); - if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, - ATTR_ORIENTATION))) { - IconFactory factory = IconFactory.getInstance(); - img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$ - } - } else if (uiName.equals(VIEW_VIEWTAG)) { - Node xmlNode = node.getXmlNode(); - if (xmlNode instanceof Element) { - String className = ((Element) xmlNode).getAttribute(ATTR_CLASS); - if (className != null && className.length() > 0) { - int index = className.lastIndexOf('.'); - if (index != -1) { - className = className.substring(index + 1); - } - img = IconFactory.getInstance().getIcon(className); - } - } - } - if (img == null) { - img = desc.getGenericIcon(); - } - if (img != null) { - if (node.hasError()) { - return new ErrorImageComposite(img).createImage(); - } else { - return img; - } - } - } + if (element instanceof UiViewElementNode) { + UiViewElementNode v = (UiViewElementNode) element; + return v.getIcon(); } return AdtPlugin.getAndroidLogo(); @@ -756,7 +736,8 @@ public class OutlinePage extends ContentOutlinePage // Temporary diagnostics code when developing GridLayout if (GridLayoutRule.sDebugGridLayout) { String namespace; - if (e.getParentNode().getNodeName().equals(GRID_LAYOUT)) { + if (e.getParentNode() != null + && e.getParentNode().getNodeName().equals(GRID_LAYOUT)) { namespace = ANDROID_URI; } else { IProject project = mGraphicalEditorPart.getProject(); @@ -1275,4 +1256,103 @@ public class OutlinePage extends ContentOutlinePage makeContributions(null, toolBarManager, null); toolBarManager.update(false); } + + /** + * Sets up a custom tooltip when hovering over tree items. It currently displays the error + * message for the lint warning associated with each node, if any (and only if the hover + * is over the icon portion). + */ + private void setupTooltip() { + final Tree tree = getTreeViewer().getTree(); + + // This is based on SWT Snippet 125 + final Listener listener = new Listener() { + Shell mTip = null; + Label mLabel = null; + + @Override + public void handleEvent(Event event) { + switch(event.type) { + case SWT.Dispose: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + case SWT.MouseMove: + if (mTip != null) { + mTip.dispose(); + mTip = null; + mLabel = null; + } + break; + case SWT.MouseHover: + if (mTip != null) { + mTip.dispose(); + mTip = null; + mLabel = null; + } + + String tooltip = null; + + TreeItem item = tree.getItem(new Point(event.x, event.y)); + if (item != null) { + Rectangle rect = item.getBounds(0); + if (event.x - rect.x > 16) { // 16: Standard width of our outline icons + return; + } + + Object data = item.getData(); + if (data != null && data instanceof CanvasViewInfo) { + LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate(); + CanvasViewInfo vi = (CanvasViewInfo) data; + IMarker marker = editor.getIssueForNode(vi.getUiViewNode()); + if (marker != null) { + tooltip = marker.getAttribute(IMarker.MESSAGE, null); + } + } + + if (tooltip != null) { + Shell shell = tree.getShell(); + Display display = tree.getDisplay(); + + Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); + Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + mTip.setBackground(bg); + FillLayout layout = new FillLayout(); + layout.marginWidth = 1; + layout.marginHeight = 1; + mTip.setLayout(layout); + mLabel = new Label(mTip, SWT.WRAP); + mLabel.setForeground(fg); + mLabel.setBackground(bg); + mLabel.setText(tooltip); + mLabel.addListener(SWT.MouseExit, this); + mLabel.addListener(SWT.MouseDown, this); + + Point pt = tree.toDisplay(rect.x, rect.y + rect.height); + Rectangle displayBounds = display.getBounds(); + // -10: Don't extend -all- the way to the edge of the screen + // which would make it look like it has been cropped + int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10; + if (availableWidth < 80) { + availableWidth = 80; + } + Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + if (size.x > availableWidth) { + size = mTip.computeSize(availableWidth, SWT.DEFAULT); + } + mTip.setBounds(pt.x, pt.y, size.x, size.y); + + mTip.setVisible(true); + } + } + } + } + }; + + tree.addListener(SWT.Dispose, listener); + tree.addListener(SWT.KeyDown, listener); + tree.addListener(SWT.MouseMove, listener); + tree.addListener(SWT.MouseHover, listener); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java index 74bc4f5..9b0d7a6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java @@ -144,15 +144,20 @@ class RenderLogger extends LayoutLog { @Override public void warning(String tag, String message, Object data) { String description = describe(message); - AdtPlugin.log(IStatus.WARNING, "%1$s: %2$s", mName, description); + boolean log = true; if (TAG_RESOURCES_FORMAT.equals(tag)) { if (description.equals("You must supply a layout_width attribute.") //$NON-NLS-1$ || description.equals("You must supply a layout_height attribute.")) {//$NON-NLS-1$ tag = TAG_MISSING_DIMENSION; + log = false; } } + if (log) { + AdtPlugin.log(IStatus.WARNING, "%1$s: %2$s", mName, description); + } + addWarning(tag, description); } 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 579ef44..23e42ef 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 @@ -30,6 +30,7 @@ import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.util.Pair; import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.Attr; import org.w3c.dom.Node; import java.awt.image.BufferedImage; @@ -117,6 +118,9 @@ public class ViewHierarchy { /** Map from nodes to canvas view infos */ private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap(); + /** Map from DOM nodes to canvas view infos */ + private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap(); + /** * Disposes the view hierarchy content. */ @@ -220,11 +224,17 @@ public class ViewHierarchy { mInvisibleParents.clear(); addInvisibleParents(mLastValidViewInfoRoot, explodedNodes); + mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size()); + for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) { + mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue()); + } + // Update the selection mCanvas.getSelectionManager().sync(); } else { mIncludedBounds = null; mInvisibleParents.clear(); + mDomNodeToView = Collections.emptyMap(); } } @@ -424,37 +434,25 @@ public class ViewHierarchy { * @return The {@link CanvasViewInfo} corresponding to the given node, or * null if no match was found. */ - public CanvasViewInfo findViewInfoFor(Node node) { - if (mLastValidViewInfoRoot != null) { - return findViewInfoForNode(node, mLastValidViewInfoRoot); - } - return null; - } - - /** - * Tries to find a child with the same view XML node in the view info sub-tree. - * Returns null if not found. - */ - private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - if (canvasViewInfo.getXmlNode() == xmlNode) { - return canvasViewInfo; - } + @Nullable + public CanvasViewInfo findViewInfoFor(@Nullable Node node) { + CanvasViewInfo vi = mDomNodeToView.get(node); - // Try to find a matching child - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoForNode(xmlNode, child); - if (v != null) { - return v; + if (vi == null) { + if (node == null) { + return null; + } else if (node.getNodeType() == Node.TEXT_NODE) { + return mDomNodeToView.get(node.getParentNode()); + } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) { + return mDomNodeToView.get(((Attr) node).getOwnerElement()); + } else if (node.getNodeType() == Node.DOCUMENT_NODE) { + return mDomNodeToView.get(node.getOwnerDocument().getDocumentElement()); } } - return null; + return vi; } - /** * Tries to find the inner most child matching the given x,y coordinates in * the view info sub-tree, starting at the last know view info root. This diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java index b87435c..fef6022 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java @@ -16,10 +16,21 @@ package com.android.ide.eclipse.adt.internal.editors.layout.uimodel; +import static com.android.ide.common.layout.LayoutConstants.ATTR_CLASS; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION; import static com.android.ide.common.layout.LayoutConstants.FQCN_FRAME_LAYOUT; - +import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; +import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL; +import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_VIEWTAG; +import static com.android.util.XmlUtils.ANDROID_URI; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 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.uimodel.UiDocumentNode; @@ -29,7 +40,11 @@ import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.IAndroidTarget; import com.android.util.XmlUtils; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; +import org.eclipse.swt.graphics.Image; +import org.w3c.dom.Element; +import org.w3c.dom.Node; /** * Specialized version of {@link UiElementNode} for the {@link ViewElementDescriptor}s. @@ -51,6 +66,9 @@ public class UiViewElementNode extends UiElementNode { */ @Override public AttributeDescriptor[] getAttributeDescriptors() { + if (!getDescriptor().syncAttributes()) { + mCachedAttributeDescriptors = null; + } if (mCachedAttributeDescriptors != null) { return mCachedAttributeDescriptors; } @@ -116,6 +134,64 @@ public class UiViewElementNode extends UiElementNode { return mCachedAttributeDescriptors; } + public Image getIcon() { + ElementDescriptor desc = getDescriptor(); + if (desc != null) { + Image img = null; + // Special case for the common case of vertical linear layouts: + // show vertical linear icon (the default icon shows horizontal orientation) + String uiName = desc.getUiName(); + IconFactory icons = IconFactory.getInstance(); + if (uiName.equals(LINEAR_LAYOUT)) { + Element e = (Element) getXmlNode(); + if (VALUE_VERTICAL.equals(e.getAttributeNS(ANDROID_URI, + ATTR_ORIENTATION))) { + IconFactory factory = icons; + img = factory.getIcon("VerticalLinearLayout"); //$NON-NLS-1$ + } + } else if (uiName.equals(VIEW_VIEWTAG)) { + Node xmlNode = getXmlNode(); + if (xmlNode instanceof Element) { + String className = ((Element) xmlNode).getAttribute(ATTR_CLASS); + if (className != null && className.length() > 0) { + int index = className.lastIndexOf('.'); + if (index != -1) { + className = className.substring(index + 1); + } + img = icons.getIcon(className); + } + } + } + if (img == null) { + img = desc.getGenericIcon(); + } + + if (img != null) { + AndroidXmlEditor editor = getEditor(); + if (editor != null) { + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + if (delegate != null) { + IMarker marker = delegate.getIssueForNode(this); + if (marker != null) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + if (severity == IMarker.SEVERITY_ERROR) { + return icons.addErrorIcon(img); + } else { + return icons.addWarningIcon(img); + } + } + } + } + + return img; + } + + return img; + } + + return AdtPlugin.getAndroidLogo(); + } + /** * Sets the parent of this UI node. * <p/> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java index 64b22a2..22737e1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java @@ -17,8 +17,10 @@ package com.android.ide.eclipse.adt.internal.editors.manifest; import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; +import static com.android.sdklib.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; import com.android.annotations.VisibleForTesting; +import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; @@ -30,9 +32,7 @@ import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.w3c.dom.Node; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * Content Assist Processor for AndroidManifest.xml @@ -51,41 +51,31 @@ public final class ManifestContentAssist extends AndroidContentAssist { protected void computeAttributeValues(List<ICompletionProposal> proposals, int offset, String parentTagName, String attributeName, Node node, String wordPrefix, boolean skipEndTag, int replaceLength) { - if (attributeName.endsWith(':' + ATTRIBUTE_MIN_SDK_VERSION)) { + if (attributeName.endsWith(ATTRIBUTE_MIN_SDK_VERSION) + || attributeName.endsWith(ATTRIBUTE_TARGET_SDK_VERSION)) { // The user is completing the minSdkVersion attribute: it should be // an integer for the API version, but we'll add full Android version // names to make it more obvious what they're selecting List<Pair<String, String>> choices = new ArrayList<Pair<String, String>>(); - // Max: Look up what versions I have + int max = AdtUtils.getHighestKnownApiLevel(); + // Look for any more recent installed versions the user may have IAndroidTarget[] targets = Sdk.getCurrent().getTargets(); - Map<String, IAndroidTarget> versionMap = new HashMap<String, IAndroidTarget>(); - List<String> codeNames = new ArrayList<String>(); - int maxVersion = 1; for (IAndroidTarget target : targets) { AndroidVersion version = target.getVersion(); int apiLevel = version.getApiLevel(); - String key; - if (version.isPreview()) { - key = version.getCodename(); - codeNames.add(key); - apiLevel--; - } else { - key = Integer.toString(apiLevel); + if (apiLevel > max) { + if (version.isPreview()) { + // Use codename, not API level, as version string for preview versions + choices.add(Pair.of(version.getCodename(), version.getCodename())); + } else { + choices.add(Pair.of(Integer.toString(apiLevel), target.getFullName())); + } } - if (apiLevel > maxVersion) { - maxVersion = apiLevel; - } - - versionMap.put(key, target); - } - for (String codeName : codeNames) { - choices.add(Pair.<String, String>of(codeName, null)); } - for (int i = maxVersion; i >= 1; i--) { - IAndroidTarget target = versionMap.get(Integer.toString(i)); - String version = target != null ? target.getFullName() : null; - choices.add(Pair.of(Integer.toString(i), version)); + for (int api = max; api >= 1; api--) { + String name = AdtUtils.getAndroidName(api); + choices.add(Pair.of(Integer.toString(api), name)); } char needTag = 0; addMatchingProposals(proposals, choices.toArray(), offset, node, wordPrefix, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java index c08a67e..2f10f68 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java @@ -109,6 +109,8 @@ public class ManifestInfo { private Map<String, String> mActivityThemes; private IAbstractFile mManifestFile; private long mLastModified; + private long mLastChecked; + private String mMinSdkName; private int mMinSdk; private int mTargetSdk; private String mApplicationIcon; @@ -163,8 +165,13 @@ public class ManifestInfo { * with respect to the manifest file */ private void sync() { - // TODO: Add a last synced timestamp so that I can avoid doing all this with rapid - // burst calls on separate methods which all call sync() first! + // Since each of the accessors call sync(), allow a bunch of immediate + // accessors to all bypass the file stat() below + long now = System.currentTimeMillis(); + if (now - mLastChecked < 50 && mManifestFile != null) { + return; + } + mLastChecked = now; if (mManifestFile == null) { IFolderWrapper projectFolder = new IFolderWrapper(mProject); @@ -186,6 +193,7 @@ public class ManifestInfo { mManifestTheme = null; mTargetSdk = 1; // Default when not specified mMinSdk = 1; // Default when not specified + mMinSdkName = ""; // Default when not specified mPackage = ""; //$NON-NLS-1$ mApplicationIcon = null; mApplicationLabel = null; @@ -252,7 +260,7 @@ public class ManifestInfo { } } - private static int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { + private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { String valueString = null; if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) { valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute); @@ -272,6 +280,10 @@ public class ManifestInfo { apiLevel = target.getVersion().getApiLevel() + 1; } } + + if (usesSdk.getTagName().equals(ATTRIBUTE_MIN_SDK_VERSION)) { + mMinSdkName = valueString; + } } return apiLevel; @@ -309,7 +321,7 @@ public class ManifestInfo { * @return a manifest theme, or null if none was registered */ @Nullable - public String getmManifestTheme() { + public String getManifestTheme() { sync(); return mManifestTheme; } @@ -388,6 +400,17 @@ public class ManifestInfo { } /** + * Returns the minimum SDK version name (which may not be a numeric string, e.g. + * it could be a codename) + * + * @return the minimum SDK version + */ + public String getMinSdkName() { + sync(); + return mMinSdkName; + } + + /** * Returns the {@link IPackageFragment} for the package registered in the manifest * * @return the {@link IPackageFragment} for the package registered in the manifest diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java index f6ec863..7085e5d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java @@ -1,5 +1,10 @@ package com.android.ide.eclipse.adt.internal.editors.ui; +import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_ERROR; +import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_WARNING; +import static org.eclipse.ui.ISharedImages.IMG_OBJS_ERROR_TSK; +import static org.eclipse.ui.ISharedImages.IMG_OBJS_WARN_TSK; + import org.eclipse.jface.resource.CompositeImageDescriptor; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.DecorationOverlayIcon; @@ -19,10 +24,30 @@ public class ErrorImageComposite extends CompositeImageDescriptor { private ImageDescriptor mErrorImageDescriptor; private Point mSize; + /** + * Creates a new {@link ErrorImageComposite} + * + * @param baseImage the base image to overlay an icon on top of + */ public ErrorImageComposite(Image baseImage) { + this(baseImage, false); + } + + /** + * Creates a new {@link ErrorImageComposite} + * + * @param baseImage the base image to overlay an icon on top of + * @param warning if true, add a warning icon, otherwise an error icon + */ + public ErrorImageComposite(Image baseImage, boolean warning) { mBaseImage = baseImage; - mErrorImageDescriptor = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor( - ISharedImages.IMG_OBJS_ERROR_TSK); + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + mErrorImageDescriptor = sharedImages.getImageDescriptor( + warning ? IMG_DEC_FIELD_WARNING : IMG_DEC_FIELD_ERROR); + if (mErrorImageDescriptor == null) { + mErrorImageDescriptor = sharedImages.getImageDescriptor( + warning ? IMG_OBJS_WARN_TSK : IMG_OBJS_ERROR_TSK); + } mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java index f2ab512..3373197 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java @@ -17,8 +17,8 @@ package com.android.ide.eclipse.adt.internal.editors.ui.tree; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; -import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import org.eclipse.jface.viewers.ILabelProvider; @@ -56,8 +56,7 @@ public class UiModelTreeLabelProvider implements ILabelProvider { Image img = desc.getCustomizedIcon(); if (img != null) { if (node != null && node.hasError()) { - //TODO: cache image - return new ErrorImageComposite(img).createImage(); + return IconFactory.getInstance().addErrorIcon(img); } else { return img; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java index f1c9b1a..4cad83e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java @@ -201,7 +201,9 @@ public class ValuesContentAssist extends AndroidContentAssist { } } } - } else if (parentNode.getNodeName().equals(ITEM_TAG)) { + } + + if (parentNode.getNodeName().equals(ITEM_TAG)) { // Completing text content inside an <item> tag: offer @resource completion. if (prefix.startsWith(PREFIX_RESOURCE_REF) || prefix.trim().length() == 0) { String[] choices = UiResourceAttributeNode.computeResourceStringMatches( diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java index 1d80c8c..e7037ff 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java @@ -41,7 +41,6 @@ import org.w3c.dom.Node; /** * Fix for adding {@code tools:ignore="id"} attributes in XML files. */ -@SuppressWarnings("restriction") // DOM model class AddSuppressAttribute implements ICompletionProposal { private final AndroidXmlEditor mEditor; private final String mId; @@ -98,7 +97,8 @@ class AddSuppressAttribute implements ICompletionProposal { } /** - * Adds any applicable suppress lint fix resolutions into the given list + * Returns a quickfix to suppress a specific lint issue id on the node corresponding to + * the given marker. * * @param editor the associated editor containing the marker * @param marker the marker to create fixes for @@ -144,4 +144,35 @@ class AddSuppressAttribute implements ICompletionProposal { Element element = (Element) node; return new AddSuppressAttribute(editor, id, marker, element, desc); } + + /** + * Returns a quickfix to suppress a given issue type on the <b>root element</b> + * of the given editor. + * + * @param editor the associated editor containing the marker + * @param marker the marker to create fixes for + * @param id the issue id + * @return a fix for this marker, or null if unable + */ + @Nullable + public static AddSuppressAttribute createFixForAll( + @NonNull AndroidXmlEditor editor, + @NonNull IMarker marker, + @NonNull String id) { + // This only applies to XML files: + String fileName = marker.getResource().getName(); + if (!fileName.endsWith(DOT_XML)) { + return null; + } + + Node node = DomUtilities.getNode(editor.getStructuredDocument(), 0); + if (node != null) { + node = node.getOwnerDocument().getDocumentElement(); + String desc = String.format("Add ignore '%1$s\' to element", id); + Element element = (Element) node; + return new AddSuppressAttribute(editor, id, marker, element, desc); + } + + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java index 68b3407..703be80 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java @@ -23,6 +23,7 @@ import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.tools.lint.checks.BuiltinIssueRegistry; @@ -78,13 +79,17 @@ import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Node; import java.io.File; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import lombok.ast.ecj.EcjTreeConverter; import lombok.ast.grammar.ParseProblem; @@ -102,6 +107,8 @@ public class EclipseLintClient extends LintClient implements IDomParser { private boolean mWasFatal; private boolean mFatalOnly; private EclipseJavaParser mJavaParser; + private boolean mCollectNodes; + private Map<Node, IMarker> mNodeMap; /** * Creates a new {@link EclipseLintClient}. @@ -119,6 +126,58 @@ public class EclipseLintClient extends LintClient implements IDomParser { mFatalOnly = fatalOnly; } + /** + * Returns true if lint should only check fatal issues + * + * @return true if lint should only check fatal issues + */ + public boolean isFatalOnly() { + return mFatalOnly; + } + + /** + * Sets whether the lint client should store associated XML nodes for each + * reported issue + * + * @param collectNodes if true, collect node positions for errors in XML + * files, retrievable via the {@link #getIssueForNode} method + */ + public void setCollectNodes(boolean collectNodes) { + mCollectNodes = collectNodes; + } + + /** + * Returns one of the issues for the given node (there could be more than one) + * + * @param node the node to look up lint issues for + * @return the marker for one of the issues found for the given node + */ + @Nullable + public IMarker getIssueForNode(@NonNull UiViewElementNode node) { + if (mNodeMap != null) { + return mNodeMap.get(node.getXmlNode()); + } + + return null; + } + + /** + * Returns a collection of nodes that have one or more lint warnings + * associated with them (retrievable via + * {@link #getIssueForNode(UiViewElementNode)}) + * + * @return a collection of nodes, which should <b>not</b> be modified by the + * caller + */ + @Nullable + public Collection<Node> getIssueNodes() { + if (mNodeMap != null) { + return mNodeMap.keySet(); + } + + return null; + } + // ----- Extends LintClient ----- @Override @@ -190,6 +249,7 @@ public class EclipseLintClient extends LintClient implements IDomParser { return null; } + @NonNull @Override public Configuration getConfiguration(Project project) { if (project != null) { @@ -249,6 +309,32 @@ public class EclipseLintClient extends LintClient implements IDomParser { if (s == Severity.FATAL) { mWasFatal = true; } + + if (mCollectNodes && location != null && marker != null) { + if (location instanceof LazyLocation) { + LazyLocation l = (LazyLocation) location; + IndexedRegion region = l.mRegion; + if (region instanceof Node) { + Node node = (Node) region; + if (node instanceof Attr) { + node = ((Attr) node).getOwnerElement(); + } + if (mNodeMap == null) { + mNodeMap = new WeakHashMap<Node, IMarker>(); + } + IMarker prev = mNodeMap.get(node); + if (prev != null) { + // Only replace the node if this node has higher priority + int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0); + if (prevSeverity < severity) { + mNodeMap.put(node, marker); + } + } else { + mNodeMap.put(node, marker); + } + } + } + } } @Override @@ -325,16 +411,6 @@ public class EclipseLintClient extends LintClient implements IDomParser { } /** - * Returns whether the given resource has one or more lint markers - * - * @param resource the resource to be checked, typically a source file - * @return true if the given resource has one or more lint markers - */ - public static boolean hasMarkers(IResource resource) { - return getMarkers(resource).length > 0; - } - - /** * Returns the lint marker for the given resource (which may be a project, folder or file) * * @param resource the resource to be checked, typically a source file @@ -532,9 +608,13 @@ public class EclipseLintClient extends LintClient implements IDomParser { * * @param shell the parent shell to attach the dialog to * @param file the file to show the errors for + * @param editor the editor for the file, if known */ - public static void showErrors(Shell shell, final IFile file) { - LintListDialog dialog = new LintListDialog(shell, file); + public static void showErrors( + @NonNull Shell shell, + @NonNull IFile file, + @Nullable IEditorPart editor) { + LintListDialog dialog = new LintListDialog(shell, file, editor); dialog.open(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java index 25e80fc..d69412b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java @@ -15,40 +15,24 @@ */ package com.android.ide.eclipse.adt.internal.lint; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_CLASS; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_JAVA; -import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; -import com.android.sdklib.SdkConstants; import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Scope; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.IJobManager; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.IDocument; import org.eclipse.swt.widgets.Shell; -import java.io.File; import java.util.ArrayList; import java.util.Collections; -import java.util.EnumSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -75,7 +59,7 @@ public class EclipseLintRunner { @Nullable IDocument doc, boolean fatalOnly) { resources = addLibraries(resources); - CheckFileJob job = (CheckFileJob) startLint(resources, source, doc, fatalOnly, + LintJob job = (LintJob) startLint(resources, source, doc, fatalOnly, false /*show*/); try { job.join(); @@ -116,12 +100,38 @@ public class EclipseLintRunner { @Nullable IDocument doc, boolean fatalOnly, boolean show) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + EclipseLintClient client = new EclipseLintClient(registry, resources, doc, fatalOnly); + return startLint(client, resources, source, show); + } + + /** + * Runs lint and updates the markers. Does not wait for the job to finish - + * just returns immediately. + * + * @param client the lint client receiving issue reports etc + * @param resources the resources (project, folder or file) to be analyzed + * @param source if checking a single source file, the source file. When + * single checking an XML file, this is typically the same as the + * file passed in the list in the first parameter, but when + * checking the .class files of a Java file for example, the + * .class file and all the inner classes of the Java file are + * passed in the first parameter, and the corresponding .java + * source file is passed here. + * @param show if true, show the results in a {@link LintViewPart} + * @return the job running lint in the background. + */ + public static Job startLint( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + boolean show) { if (resources != null && !resources.isEmpty()) { resources = addLibraries(resources); cancelCurrentJobs(false); - CheckFileJob job = new CheckFileJob(resources, source, doc, fatalOnly); + LintJob job = new LintJob(client, resources, source); job.schedule(); if (show) { @@ -162,16 +172,10 @@ public class EclipseLintRunner { return true; } - /** Returns the current lint jobs, if any (never returns null but array may be empty) */ - static Job[] getCurrentJobs() { - IJobManager jobManager = Job.getJobManager(); - return jobManager.find(CheckFileJob.FAMILY_RUN_LINT); - } - /** Cancels the current lint jobs, if any, and optionally waits for them to finish */ static void cancelCurrentJobs(boolean wait) { // Cancel any current running jobs first - Job[] currentJobs = getCurrentJobs(); + Job[] currentJobs = LintJob.getCurrentJobs(); for (Job job : currentJobs) { job.cancel(); } @@ -229,132 +233,4 @@ public class EclipseLintRunner { return resources; } - - private static final class CheckFileJob extends Job { - /** Job family */ - private static final Object FAMILY_RUN_LINT = new Object(); - private final List<? extends IResource> mResources; - private final IResource mSource; - private final IDocument mDocument; - private LintDriver mLint; - private boolean mFatal; - private boolean mFatalOnly; - - private CheckFileJob( - @NonNull List<? extends IResource> resources, - @Nullable IResource source, - @Nullable IDocument doc, - boolean fatalOnly) { - super("Running Android Lint"); - mResources = resources; - mSource = source; - mDocument = doc; - mFatalOnly = fatalOnly; - } - - @Override - public boolean belongsTo(Object family) { - return family == FAMILY_RUN_LINT; - } - - @Override - protected void canceling() { - super.canceling(); - if (mLint != null) { - mLint.cancel(); - } - } - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN); - IssueRegistry registry = EclipseLintClient.getRegistry(); - EnumSet<Scope> scope = null; - List<File> files = new ArrayList<File>(mResources.size()); - for (IResource resource : mResources) { - File file = AdtUtils.getAbsolutePath(resource).toFile(); - files.add(file); - - if (resource instanceof IProject && mSource == null) { - scope = Scope.ALL; - } else { - String name = resource.getName(); - if (AdtUtils.endsWithIgnoreCase(name, DOT_XML)) { - if (name.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { - scope = EnumSet.of(Scope.MANIFEST); - } else { - scope = Scope.RESOURCE_FILE_SCOPE; - } - } else if (name.endsWith(DOT_JAVA) && resource instanceof IFile) { - if (scope != null) { - if (!scope.contains(Scope.JAVA_FILE)) { - scope = EnumSet.copyOf(scope); - scope.add(Scope.JAVA_FILE); - } - } else { - scope = Scope.JAVA_FILE_SCOPE; - } - } else if (name.endsWith(DOT_CLASS) && resource instanceof IFile) { - if (scope != null) { - if (!scope.contains(Scope.CLASS_FILE)) { - scope = EnumSet.copyOf(scope); - scope.add(Scope.CLASS_FILE); - } - } else { - scope = Scope.CLASS_FILE_SCOPE; - } - } else { - return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, - "Only XML files are supported for single file lint", null); //$NON-NLS-1$ - } - } - } - if (scope == null) { - scope = Scope.ALL; - } - if (mSource == null) { - assert !Scope.checkSingleFile(scope) : scope + " with " + mResources; - } - // Check single file? - if (mSource != null) { - // Delete specific markers - IMarker[] markers = EclipseLintClient.getMarkers(mSource); - for (IMarker marker : markers) { - String id = marker.getAttribute(MARKER_CHECKID_PROPERTY, ""); //$NON-NLS-1$ - Issue issue = registry.getIssue(id); - if (issue == null) { - continue; - } - if (issue.isAdequate(scope)) { - marker.delete(); - } - } - } else { - EclipseLintClient.clearMarkers(mResources); - } - - EclipseLintClient client = new EclipseLintClient(registry, mResources, - mDocument, mFatalOnly); - mLint = new LintDriver(registry, client); - mLint.analyze(files, scope); - mFatal = client.hasFatalErrors(); - return Status.OK_STATUS; - } catch (Exception e) { - return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, - "Failed", e); //$NON-NLS-1$ - } finally { - if (monitor != null) { - monitor.done(); - } - } - } - - /** - * Returns true if a fatal error was encountered - */ - boolean isFatal() { - return mFatal; - } - } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java index 8da3465..646d752 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.lint; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.tools.lint.client.api.Configuration; @@ -47,6 +48,7 @@ class GlobalLintConfiguration extends Configuration { * * @return the singleton configuration */ + @NonNull public static GlobalLintConfiguration get() { return sInstance; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java new file mode 100644 index 0000000..aaf55c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.ide.eclipse.adt.AdtConstants.DOT_CLASS; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_JAVA; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.sdklib.SdkConstants; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; + +import java.io.File; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** Job to check lint on a set of resources */ +final class LintJob extends Job { + /** Job family */ + private static final Object FAMILY_RUN_LINT = new Object(); + private final EclipseLintClient mClient; + private final List<? extends IResource> mResources; + private final IResource mSource; + private LintDriver mLint; + private boolean mFatal; + + LintJob( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source) { + super("Running Android Lint"); + mClient = client; + mResources = resources; + mSource = source; + } + + @Override + public boolean belongsTo(Object family) { + return family == FAMILY_RUN_LINT; + } + + @Override + protected void canceling() { + super.canceling(); + if (mLint != null) { + mLint.cancel(); + } + } + + @Override + @NonNull + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN); + IssueRegistry registry = EclipseLintClient.getRegistry(); + EnumSet<Scope> scope = null; + List<File> files = new ArrayList<File>(mResources.size()); + for (IResource resource : mResources) { + File file = AdtUtils.getAbsolutePath(resource).toFile(); + files.add(file); + + if (resource instanceof IProject && mSource == null) { + scope = Scope.ALL; + } else { + String name = resource.getName(); + if (AdtUtils.endsWithIgnoreCase(name, DOT_XML)) { + if (name.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { + scope = EnumSet.of(Scope.MANIFEST); + } else { + scope = Scope.RESOURCE_FILE_SCOPE; + } + } else if (name.endsWith(DOT_JAVA) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.JAVA_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.JAVA_FILE); + } + } else { + scope = Scope.JAVA_FILE_SCOPE; + } + } else if (name.endsWith(DOT_CLASS) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.CLASS_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.CLASS_FILE); + } + } else { + scope = Scope.CLASS_FILE_SCOPE; + } + } else { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Only XML & Java files are supported for single file lint", null); //$NON-NLS-1$ + } + } + } + if (scope == null) { + scope = Scope.ALL; + } + if (mSource == null) { + assert !Scope.checkSingleFile(scope) : scope + " with " + mResources; + } + // Check single file? + if (mSource != null) { + // Delete specific markers + IMarker[] markers = EclipseLintClient.getMarkers(mSource); + for (IMarker marker : markers) { + String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, ""); + Issue issue = registry.getIssue(id); + if (issue == null) { + continue; + } + if (issue.isAdequate(scope)) { + marker.delete(); + } + } + } else { + EclipseLintClient.clearMarkers(mResources); + } + + mLint = new LintDriver(registry, mClient); + mLint.analyze(files, scope); + mFatal = mClient.hasFatalErrors(); + return Status.OK_STATUS; + } catch (Exception e) { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Failed", e); //$NON-NLS-1$ + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + + /** + * Returns true if a fatal error was encountered + * + * @return true if a fatal error was encountered + */ + public boolean isFatal() { + return mFatal; + } + + /** + * Returns the associated lint client + * + * @return the associated lint client + */ + @NonNull + public EclipseLintClient getLintClient() { + return mClient; + } + + /** Returns the current lint jobs, if any (never returns null but array may be empty) */ + @NonNull + static Job[] getCurrentJobs() { + IJobManager jobManager = Job.getJobManager(); + return jobManager.find(LintJob.FAMILY_RUN_LINT); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java index 561c5fd..3b01e05 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java @@ -261,6 +261,13 @@ class LintList extends Composite implements IResourceChangeListener, ControlList updateColumnWidths(); // in case mSingleFile changed } + /** Select the first item */ + public void selectFirst() { + if (mTree.getItemCount() > 0) { + mTree.select(mTree.getItem(0)); + } + } + private List<IMarker> getMarkers() { mErrorCount = mWarningCount = 0; List<IMarker> markerList = new ArrayList<IMarker>(); @@ -585,11 +592,25 @@ class LintList extends Composite implements IResourceChangeListener, ControlList /** Expands all nodes */ public void expandAll() { mTreeViewer.expandAll(); + + if (mExpandedIds == null) { + mExpandedIds = new HashSet<String>(); + } + IMarker[] topMarkers = mContentProvider.getTopMarkers(); + if (topMarkers != null) { + for (IMarker marker : topMarkers) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + mExpandedIds.add(id); + } + } + } } /** Collapses all nodes */ public void collapseAll() { mTreeViewer.collapseAll(); + mExpandedIds = null; } // ---- Column Persistence ---- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java index d39916f..147327d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java @@ -15,9 +15,12 @@ */ package com.android.ide.eclipse.adt.internal.lint; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; @@ -45,22 +48,30 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; @SuppressWarnings("restriction") // WST DOM access class LintListDialog extends TitleAreaDialog implements SelectionListener { - private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ - private IFile mFile; + private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$ + private final IFile mFile; + private final IEditorPart mEditor; private Button mFixButton; private Button mIgnoreButton; + private Button mIgnoreAllButton; private Button mShowButton; private Text mDetailsText; private Button mIgnoreTypeButton; private LintList mList; - LintListDialog(Shell parentShell, IFile file) { + LintListDialog( + @NonNull Shell parentShell, + @NonNull IFile file, + @Nullable IEditorPart editor) { super(parentShell); - this.mFile = file; + mFile = file; + mEditor = editor; } @Override @@ -80,7 +91,8 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { Control contents = super.createContents(parent); setTitle("Lint Warnings in Layout"); setMessage("Lint Errors found for the current layout:"); - setTitleImage(AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE).createImage()); + setTitleImage(IconFactory.getInstance().getIcon(PROJECT_LOGO_LARGE)); + return contents; } @@ -97,31 +109,42 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { if (page.getActivePart() != null) { site = page.getActivePart().getSite(); } + mList = new LintList(site, container, null /*memento*/, true /*singleFile*/); - mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 5)); + mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 6)); mShowButton = new Button(container, SWT.NONE); mShowButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); mShowButton.setText("Show"); + mShowButton.setToolTipText("Opens the editor to reveal the XML with the issue"); mShowButton.addSelectionListener(this); + mFixButton = new Button(container, SWT.NONE); + mFixButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mFixButton.setText("Fix"); + mFixButton.setToolTipText("Automatically corrects the problem, if possible"); + mFixButton.setEnabled(false); + mFixButton.addSelectionListener(this); + mIgnoreButton = new Button(container, SWT.NONE); mIgnoreButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mIgnoreButton.setText("Ignore"); - mIgnoreButton.setEnabled(false); + mIgnoreButton.setText("Suppress Issue"); + mIgnoreButton.setToolTipText("Adds a special attribute in the layout to suppress this specific warning"); mIgnoreButton.addSelectionListener(this); + mIgnoreAllButton = new Button(container, SWT.NONE); + mIgnoreAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mIgnoreAllButton.setText("Suppress in Layout"); + mIgnoreAllButton.setEnabled(mEditor instanceof AndroidXmlEditor); + mIgnoreAllButton.setToolTipText("Adds an attribute on the root element to suppress all issues of this type in this layout"); + mIgnoreAllButton.addSelectionListener(this); + mIgnoreTypeButton = new Button(container, SWT.NONE); mIgnoreTypeButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mIgnoreTypeButton.setText("Ignore Type"); + mIgnoreTypeButton.setText("Disable Issue Type"); + mIgnoreTypeButton.setToolTipText("Turns off checking for this type of error everywhere"); mIgnoreTypeButton.addSelectionListener(this); - mFixButton = new Button(container, SWT.NONE); - mFixButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); - mFixButton.setText("Fix"); - mFixButton.setEnabled(false); - mFixButton.addSelectionListener(this); - new Label(container, SWT.NONE); mDetailsText = new Text(container, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP @@ -138,6 +161,7 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { mList.addSelectionListener(this); mList.setResources(Collections.<IResource>singletonList(mFile)); + mList.selectFirst(); if (mList.getSelectedMarkers().size() > 0) { updateSelectionState(); } @@ -205,6 +229,28 @@ class LintListDialog extends TitleAreaDialog implements SelectionListener { LintFixGenerator.suppressDetector(id, true, mFile, true /*all*/); } } + } else if (source == mIgnoreButton) { + for (IMarker marker : mList.getSelectedMarkers()) { + LintFixGenerator.addSuppressAnnotation(marker); + } + } else if (source == mIgnoreAllButton) { + Set<String> ids = new HashSet<String>(); + for (IMarker marker : mList.getSelectedMarkers()) { + String id = EclipseLintClient.getId(marker); + if (id != null && !ids.contains(id)) { + ids.add(id); + if (mEditor instanceof AndroidXmlEditor) { + AndroidXmlEditor editor = (AndroidXmlEditor) mEditor; + AddSuppressAttribute fix = AddSuppressAttribute.createFixForAll(editor, + marker, id); + if (fix != null) { + IStructuredDocument document = editor.getStructuredDocument(); + fix.apply(document); + } + } + } + } + mList.refresh(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java index 39c6d25..3761fde 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java @@ -291,7 +291,7 @@ public class LintViewPart extends ViewPart implements SelectionListener, IJobCha } private void refreshStopIcon() { - Job[] currentJobs = EclipseLintRunner.getCurrentJobs(); + Job[] currentJobs = LintJob.getCurrentJobs(); if (currentJobs.length > 0) { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); mRefreshAction.setImageDescriptor(sharedImages.getImageDescriptor( @@ -466,7 +466,7 @@ public class LintViewPart extends ViewPart implements SelectionListener, IJobCha workbench.saveAllEditors(false /*confirm*/); } - Job[] jobs = EclipseLintRunner.getCurrentJobs(); + Job[] jobs = LintJob.getCurrentJobs(); if (jobs.length > 0) { EclipseLintRunner.cancelCurrentJobs(false); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java index 18f3db3..a0f1262 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java @@ -21,13 +21,11 @@ import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.IconFactory; -import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.tools.lint.detector.api.LintUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.IAdaptable; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.ui.JavaElementLabelProvider; import org.eclipse.jface.action.Action; @@ -39,7 +37,6 @@ import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IObjectActionDelegate; @@ -51,8 +48,6 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.ITextEditor; import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; import java.util.List; /** @@ -77,81 +72,16 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, if (!projects.isEmpty()) { EclipseLintRunner.startLint(projects, null, null, false /*fatalOnly*/, true /*show*/); - } else { - MessageDialog.openWarning(AdtPlugin.getDisplay().getActiveShell(), "Lint", - "Could not run Lint: Select a project first."); } } /** Returns the Android project(s) to apply a lint run to. */ static List<IProject> getProjects(ISelection selection, boolean warn) { - List<IProject> projects = new ArrayList<IProject>(); - - if (selection instanceof IStructuredSelection) { - IStructuredSelection structuredSelection = (IStructuredSelection) selection; - // get the unique selected item. - Iterator<?> iterator = structuredSelection.iterator(); - while (iterator.hasNext()) { - Object element = iterator.next(); - - // First look up the resource (since some adaptables - // provide an IResource but not an IProject, and we can - // always go from IResource to IProject) - IResource resource = null; - if (element instanceof IResource) { // may include IProject - resource = (IResource) element; - } else if (element instanceof IAdaptable) { - IAdaptable adaptable = (IAdaptable)element; - Object adapter = adaptable.getAdapter(IResource.class); - resource = (IResource) adapter; - } - - // get the project object from it. - IProject project = null; - if (resource != null) { - project = resource.getProject(); - } else if (element instanceof IAdaptable) { - project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); - } - - if (project != null && !projects.contains(project)) { - projects.add(project); - } - } - } - - if (projects.isEmpty()) { - // Try to look at the active editor instead - IFile file = AdtUtils.getActiveFile(); - if (file != null) { - projects.add(file.getProject()); - } - } - - if (projects.isEmpty()) { - // If we didn't find a default project based on the selection, check how many - // open Android projects we can find in the current workspace. If there's only - // one, we'll just select it by default. - IJavaProject[] open = AdtUtils.getOpenAndroidProjects(); - for (IJavaProject project : open) { - projects.add(project.getProject()); - } - } else { - // Make sure all the projects are Android projects - for (IProject project : projects) { - if (!BaseProjectHelper.isAndroidProject(project)) { - if (warn) { - MessageDialog.openWarning(AdtPlugin.getDisplay().getActiveShell(), - "Lint", "Select Android projects."); - } - return Collections.emptyList(); - } - } - } + List<IProject> projects = AdtUtils.getSelectedProjects(selection); if (projects.isEmpty() && warn) { MessageDialog.openWarning(AdtPlugin.getDisplay().getActiveShell(), "Lint", - "Could not run Lint: Select a project first."); + "Could not run Lint: Select an Android project first."); } return projects; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java index 827f8a4..b2d7361 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java @@ -338,6 +338,10 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer prefs.setLintOnSave(mCheckFileCheckbox.getSelection()); } + if (mConfiguration == null) { + return; + } + mConfiguration.startBulkEditing(); try { // Severities diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java index 8f6de3a..45590b7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/CompatibilityLibraryHelper.java @@ -163,7 +163,7 @@ public class CompatibilityLibraryHelper { for (IJavaProject javaProject : AdtUtils.getOpenAndroidProjects()) { IProject project = javaProject.getProject(); ProjectState state = Sdk.getProjectState(project); - if (state.isLibrary()) { + if (state != null && state.isLibrary()) { ManifestInfo manifestInfo = ManifestInfo.get(project); if (manifestInfo.getPackage().equals("android.support.v7.gridlayout")) { //$NON-NLS-1$ return project; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java index de27011..35ef1c7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java @@ -171,6 +171,7 @@ public class WelcomeWizardPage extends WizardPage implements ModifyListener, Sel String file = dialog.open(); String path = mExistingDirText.getText().trim(); if (path.length() > 0) { + // TODO: Shouldn't this be done before the open() call? dialog.setFilterPath(path); } if (file != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java index 0d7e9ae..cda13b5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java @@ -52,7 +52,7 @@ import java.io.FileFilter; import java.net.URI; /** Page where you choose the application name, activity name, and optional test project info */ -class ApplicationInfoPage extends WizardPage implements SelectionListener, ModifyListener, +public class ApplicationInfoPage extends WizardPage implements SelectionListener, ModifyListener, ITargetChangeListener { private static final String JDK_15 = "1.5"; //$NON-NLS-1$ private final static String DUMMY_PACKAGE = "your.package.namespace"; @@ -637,7 +637,7 @@ class ApplicationInfoPage extends WizardPage implements SelectionListener, Modif return null; } - private IStatus validatePackage(String packageFieldContents) { + public static IStatus validatePackage(String packageFieldContents) { // Validate package if (packageFieldContents == null || packageFieldContents.length() == 0) { return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, @@ -677,8 +677,17 @@ class ApplicationInfoPage extends WizardPage implements SelectionListener, Modif return null; } + return validateActivity(mValues.activityName); + } + + /** + * Validates the given activity name + * + * @param activityFieldContents the activity name to validate + * @return a status for whether the activity name is valid + */ + public static IStatus validateActivity(String activityFieldContents) { // Validate activity field - String activityFieldContents = mValues.activityName; if (activityFieldContents == null || activityFieldContents.length() == 0) { return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Activity name must be specified."); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java index 5b557ac..2dc7c71 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java @@ -360,7 +360,6 @@ public class NewProjectCreator { String pkg = mValues.mode == Mode.TEST ? mValues.packageName : mValues.testPackageName; - parameters.put(PARAM_PROJECT, projectName); parameters.put(PARAM_PACKAGE, pkg); parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); @@ -481,7 +480,8 @@ public class NewProjectCreator { mainData.getProject(), mainData.getDescription(), mainData.getParameters(), - mainData.getDictionary()); + mainData.getDictionary(), + null); if (mainProject != null) { final IJavaProject javaProject = JavaCore.create(mainProject); @@ -512,7 +512,8 @@ public class NewProjectCreator { testData.getProject(), testData.getDescription(), parameters, - testData.getDictionary()); + testData.getDictionary(), + null); if (testProject != null) { final IJavaProject javaProject = JavaCore.create(testProject); Display.getDefault().syncExec(new Runnable() { @@ -556,7 +557,8 @@ public class NewProjectCreator { IProject project, IProjectDescription description, Map<String, Object> parameters, - Map<String, String> dictionary) + Map<String, String> dictionary, + Runnable projectPopulator) throws CoreException, IOException, StreamException { // get the project target @@ -587,6 +589,10 @@ public class NewProjectCreator { addDefaultDirectories(project, RES_DIRECTORY, RES_DENSITY_ENABLED_DIRECTORIES, monitor); } + if (projectPopulator != null) { + projectPopulator.run(); + } + // Setup class path: mark folders as source folders IJavaProject javaProject = JavaCore.create(project); setupSourceFolders(javaProject, sourceFolders, monitor); @@ -662,6 +668,28 @@ public class NewProjectCreator { return project; } + public static IProject create( + IProgressMonitor monitor, + IProject project, + IAndroidTarget target, + Runnable projectPopulator) + throws CoreException, IOException, StreamException { + NewProjectCreator creator = new NewProjectCreator(null, null); + + Map<String, String> dictionary = null; + Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_SDK_TARGET, target); + parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES); + parameters.put(PARAM_IS_NEW_PROJECT, false); + parameters.put(PARAM_SAMPLE_LOCATION, null); + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + return creator.createEclipseProject(monitor, project, description, parameters, + dictionary, projectPopulator); + } + /** * Adds default directories to the project. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java index d4c342c..7592c58 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java @@ -66,7 +66,7 @@ import java.util.regex.Pattern; * Initial page shown when creating projects which asks for the project name, * the the location of the project, working sets, etc. */ -class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener { +public class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener { private final NewProjectWizardState mValues; /** Flag used when setting button/text state manually to ignore listener updates */ private boolean mIgnore; @@ -601,7 +601,7 @@ class ProjectNamePage extends WizardPage implements SelectionListener, ModifyLis return null; } - static IStatus validateProjectName(String projectName) { + public static IStatus validateProjectName(String projectName) { if (projectName == null || projectName.length() == 0) { return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Project name must be specified"); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java new file mode 100644 index 0000000..9a61b4f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard.ACTIVITY_TEMPLATES; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.List; + +import java.io.InputStream; + +class ActivityPage extends WizardPage implements SelectionListener { + private final NewProjectWizardState mValues; + private List mList; + private Button mCreateToggle; + + private boolean mIgnore; + private boolean mShown; + private ImageControl mPreview; + private Image mPreviewImage; + private Label mHeading; + private Label mDescription; + + /** + * Create the wizard. + */ + ActivityPage(NewProjectWizardState values) { + super("activityPage"); //$NON-NLS-1$ + mValues = values; + + setTitle("Create Activity"); + setDescription("Select whether to create an activity, and if so, what kind of activity."); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + container.setLayout(new GridLayout(3, false)); + + mCreateToggle = new Button(container, SWT.CHECK); + mCreateToggle.setSelection(true); + mCreateToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mCreateToggle.setText("Create Activity"); + mCreateToggle.addSelectionListener(this); + + mList = new List(container, SWT.BORDER | SWT.V_SCROLL); + mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + mList.setItems(ACTIVITY_TEMPLATES); + int index = -1; + for (int i = 0; i < ACTIVITY_TEMPLATES.length; i++) { + if (ACTIVITY_TEMPLATES[i].equals(mValues.activityValues.getTemplateName())) { + index = i; + break; + } + } + if (index == -1) { + mValues.activityValues.setTemplateName(ACTIVITY_TEMPLATES[0]); + index = 0; + } + mList.setSelection(index); + mList.addSelectionListener(this); + + // Preview + mPreview = new ImageControl(container, SWT.NONE, null); + GridData gd_mImage = new GridData(SWT.CENTER, SWT.CENTER, false, false, 1, 1); + gd_mImage.widthHint = PREVIEW_WIDTH + 2 * PREVIEW_PADDING; + mPreview.setLayoutData(gd_mImage); + new Label(container, SWT.NONE); + + mHeading = new Label(container, SWT.NONE); + mHeading.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + new Label(container, SWT.NONE); + + mDescription = new Label(container, SWT.WRAP); + mDescription.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1)); + + Font font = JFaceResources.getFontRegistry().getBold(JFaceResources.BANNER_FONT); + if (font != null) { + mHeading.setFont(font); + } + + setPreview(mValues.activityValues.getTemplateName()); + } + + private void setPreview(String templateName) { + Image oldImage = mPreviewImage; + mPreviewImage = null; + + String title = ""; + String description = ""; + TemplateMetadata template = TemplateHandler.getTemplate(templateName); + if (template != null) { + String thumb = template.getThumbnailPath(); + if (thumb != null && !thumb.isEmpty()) { + String filePath = TemplateHandler.getTemplatePath(templateName) + '/' + thumb; + InputStream input = AdtPlugin.readEmbeddedFileAsStream(filePath); + if (input != null) { + try { + mPreviewImage = new Image(getControl().getDisplay(), input); + input.close(); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + } + title = template.getTitle(); + description = template.getDescription(); + } + + mHeading.setText(title); + mDescription.setText(description); + mPreview.setImage(mPreviewImage); + mPreview.fitToWidth(PREVIEW_WIDTH); + + if (oldImage != null) { + oldImage.dispose(); + } + + Composite parent = (Composite) getControl(); + parent.layout(true, true); + parent.redraw(); + } + + @Override + public void dispose() { + super.dispose(); + + if (mPreviewImage != null) { + mPreviewImage.dispose(); + mPreviewImage = null; + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + mShown = true; + try { + mIgnore = true; + mCreateToggle.setSelection(mValues.createActivity); + } finally { + mIgnore = false; + } + } + + validatePage(); + } + + + private void validatePage() { + IStatus status = null; + + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mList.getSelectionCount() < 1) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Select an activity type"); + } + } + + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + @Override + public boolean isPageComplete() { + if (!mValues.createAppSkeleton) { + return true; + } + + // Ensure that the Finish button isn't enabled until + // the user has reached and completed this page + if (!mShown) { + return false; + } + + return super.isPageComplete(); + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mCreateToggle) { + mValues.createActivity = mCreateToggle.getSelection(); + mList.setEnabled(mValues.createActivity); + } else if (source == mList) { + int index = mList.getSelectionIndex(); + String[] items = mList.getItems(); + if (index >= 0 && index < items.length) { + String templateName = items[index]; + mValues.activityValues.setTemplateName(templateName); + setPreview(templateName); + } + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/AppSkeletonPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/AppSkeletonPage.java new file mode 100644 index 0000000..ed45a7b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/AppSkeletonPage.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; + +class AppSkeletonPage extends WizardPage implements SelectionListener { + private final NewProjectWizardState mValues; + + private Button mActionBarToggle; + private Button mAboutToggle; + private Button mSettingsToggle; + private Button mPhoneToggle; + private Button mTabletToggle; + + private boolean mIgnore; + private Button mCreateIconToggle; + + AppSkeletonPage(NewProjectWizardState values) { + super("appSkeletonPage"); + mValues = values; + setTitle("Configure Application Skeleton"); + setDescription("Select which platforms to target and what to include in the app"); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + GridLayout gl_container = new GridLayout(2, false); + gl_container.horizontalSpacing = 10; + container.setLayout(gl_container); + Label targetLabel = new Label(container, SWT.NONE); + targetLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + targetLabel.setText("Target:"); + mPhoneToggle = new Button(container, SWT.CHECK); + mPhoneToggle.setSelection(true); + mPhoneToggle.setText("Phones"); + mPhoneToggle.setEnabled(false); + mPhoneToggle.addSelectionListener(this); + new Label(container, SWT.NONE); + mTabletToggle = new Button(container, SWT.CHECK); + mTabletToggle.setSelection(true); + mTabletToggle.setText("Tablets"); + mTabletToggle.setEnabled(false); + mTabletToggle.addSelectionListener(this); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + Label uiLabel = new Label(container, SWT.NONE); + uiLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + uiLabel.setText("User Interface:"); + mActionBarToggle = new Button(container, SWT.CHECK); + mActionBarToggle.setSelection(true); + mActionBarToggle.setText("Action Bar"); + mActionBarToggle.setEnabled(false); + mActionBarToggle.addSelectionListener(this); + new Label(container, SWT.NONE); + mSettingsToggle = new Button(container, SWT.CHECK); + mSettingsToggle.setSelection(true); + mSettingsToggle.setText("Settings"); + mSettingsToggle.setEnabled(false); + mSettingsToggle.addSelectionListener(this); + new Label(container, SWT.NONE); + mAboutToggle = new Button(container, SWT.CHECK); + mAboutToggle.setSelection(true); + mAboutToggle.setText("About"); + mAboutToggle.setEnabled(false); + mAboutToggle.addSelectionListener(this); + + new Label(container, SWT.NONE); + mCreateIconToggle = new Button(container, SWT.CHECK); + mCreateIconToggle.setSelection(mValues.createIcon); + mCreateIconToggle.setText("Create custom launcher icon"); + mCreateIconToggle.addSelectionListener(this); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + validatePage(); + } + + private void validatePage() { + IStatus status = null; + + if (status == null || status.getSeverity() != IStatus.ERROR) { + // Ensure that you're choosing at least one UI target + if (!mValues.phone && !mValues.tablet) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "You must choose at least one target (tablet or phone)"); + } + } + + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mPhoneToggle) { + mValues.phone = mPhoneToggle.getSelection(); + } else if (source == mTabletToggle) { + mValues.tablet = mTabletToggle.getSelection(); + } else if (source == mCreateIconToggle) { + mValues.createIcon = mCreateIconToggle.getSelection(); + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java new file mode 100644 index 0000000..a6f2a9e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX; + +import com.android.ide.eclipse.adt.AdtUtils; + +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import java.util.List; + +/** + * Method invoked by FreeMarker to convert an Activity class name into + * a suitable layout name. + */ +public class FmActivityToLayoutMethod implements TemplateMethodModel { + @Override + public TemplateModel exec(List args) throws TemplateModelException { + if (args.size() != 1) { + throw new TemplateModelException("Wrong arguments"); + } + + String activityName = args.get(0).toString(); + + // Strip off the end portion of the activity name. The user might be typing + // the activity name such that only a portion has been entered so far (e.g. + // "MainActivi") and we want to chop off that portion too such that we don't + // offer a layout name partially containing the activity suffix (e.g. "main_activi"). + int suffixStart = activityName.lastIndexOf(ACTIVITY_NAME_SUFFIX.charAt(0)); + if (suffixStart != -1 && activityName.regionMatches(suffixStart, ACTIVITY_NAME_SUFFIX, 0, + activityName.length() - suffixStart)) { + activityName = activityName.substring(0, suffixStart); + } + assert !activityName.endsWith(ACTIVITY_NAME_SUFFIX) : activityName; + + // Convert CamelCase convention used in activity class names to underlined convention + // used in layout name: + String name = AdtUtils.camelCaseToUnderlines(activityName); + + return new SimpleScalar(name); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java new file mode 100644 index 0000000..f017aa7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import com.android.ide.eclipse.adt.AdtUtils; + +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import java.util.List; + +/** + * Method invoked by FreeMarker to convert an underscore name into a CamelCase name. + */ +public class FmCamelCaseToUnderscoreMethod implements TemplateMethodModel { + @Override + public TemplateModel exec(List args) throws TemplateModelException { + if (args.size() != 1) { + throw new TemplateModelException("Wrong arguments"); + } + return new SimpleScalar(AdtUtils.underlinesToCamelCase(args.get(0).toString())); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java new file mode 100644 index 0000000..f3dd9cd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.AdtUtils.extractClassName; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX; + +import com.android.ide.eclipse.adt.AdtUtils; + +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import java.util.List; + +/** + * Method invoked by FreeMarker to convert a layout name into an appropriate + * Activity class. + */ +public class FmLayoutToActivityMethod implements TemplateMethodModel { + @Override + public TemplateModel exec(List args) throws TemplateModelException { + if (args.size() != 1) { + throw new TemplateModelException("Wrong arguments"); + } + + String name = args.get(0).toString(); + name = AdtUtils.underlinesToCamelCase(name); + String className = extractClassName(name); + if (className == null) { + className = "My"; + } + String activityName = className + ACTIVITY_NAME_SUFFIX; + + return new SimpleScalar(activityName); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java new file mode 100644 index 0000000..60a6531 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import java.util.List; + +/** + * Method invoked by FreeMarker to convert a package name (foo.bar) into + * a slashed path (foo/bar) + */ +public class FmSlashedPackageNameMethod implements TemplateMethodModel { + + @Override + public TemplateModel exec(List args) throws TemplateModelException { + if (args.size() != 1) { + throw new TemplateModelException("Wrong arguments"); + } + + return new SimpleScalar(args.get(0).toString().replace('.', '/')); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java new file mode 100644 index 0000000..94a41da --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import com.android.ide.eclipse.adt.AdtUtils; + +import freemarker.template.SimpleScalar; +import freemarker.template.TemplateMethodModel; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +import java.util.List; + +/** + * Method invoked by FreeMarker to convert a CamelCase word into + * underscore_names. + */ +public class FmUnderscoreToCamelCaseMethod implements TemplateMethodModel { + @Override + public TemplateModel exec(List args) throws TemplateModelException { + if (args.size() != 1) { + throw new TemplateModelException("Wrong arguments"); + } + return new SimpleScalar(AdtUtils.camelCaseToUnderlines(args.get(0).toString())); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java new file mode 100644 index 0000000..0c40fab --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + + +import static com.android.ide.eclipse.adt.AdtUtils.extractClassName; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplatePage.WIZARD_PAGE_WIDTH; +import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled; + +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage; +import com.android.ide.eclipse.adt.internal.wizards.newproject.ProjectNamePage; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.google.common.collect.Maps; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +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.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import lombok.ast.libs.org.parboiled.google.collect.Lists; + +/** + * First wizard page in the "New Project From Template" wizard + */ +public class NewProjectPage extends WizardPage + implements ModifyListener, SelectionListener, FocusListener { + private static final String SAMPLE_PACKAGE_PREFIX = "com.example."; //$NON-NLS-1$ + /** Suffix added by default to activity names */ + static final String ACTIVITY_NAME_SUFFIX = "Activity"; //$NON-NLS-1$ + private static final int INITIAL_MIN_SDK = 8; + + private final NewProjectWizardState mValues; + private Map<String, Integer> mMinNameToApi; + + private Text mProjectText; + private Text mPackageText; + private Text mApplicationText; + private Combo mMinSdkCombo; + private Button mCompatToggle; + + private boolean mIgnore; + private Combo mBuildSdkCombo; + private Button mChooseSdkButton; + private Button mSkeletonToggle; + private Label mHelpIcon; + private Label mTipLabel; + + private ControlDecoration mApplicationDec; + private ControlDecoration mProjectDec; + private ControlDecoration mPackageDec; + private ControlDecoration mBuildTargetDec; + private ControlDecoration mMinSdkDec; + + NewProjectPage(NewProjectWizardState values) { + super("newAndroidApp"); //$NON-NLS-1$ + mValues = values; + setTitle("New Android Application"); + setDescription("Creates a new Android Application"); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + GridLayout gl_container = new GridLayout(4, false); + gl_container.horizontalSpacing = 10; + container.setLayout(gl_container); + + Label applicationLabel = new Label(container, SWT.NONE); + applicationLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1)); + applicationLabel.setText("Application Name:"); + + mApplicationText = new Text(container, SWT.BORDER); + mApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mApplicationText.addModifyListener(this); + mApplicationText.addFocusListener(this); + mApplicationDec = createFieldDecoration(mApplicationText, + "The application name is shown in the Play store, as well as in the " + + "Manage Application list in settings."); + + Label projectLabel = new Label(container, SWT.NONE); + projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1)); + projectLabel.setText("Project Name:"); + mProjectText = new Text(container, SWT.BORDER); + mProjectText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mProjectText.addModifyListener(this); + mProjectText.addFocusListener(this); + mProjectDec = createFieldDecoration(mProjectText, + "The project name is only used by Eclipse, but must be unique within the " + + "workspace. This can typically be the same as the application name."); + + Label packageLabel = new Label(container, SWT.NONE); + packageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1)); + packageLabel.setText("Package Name:"); + + mPackageText = new Text(container, SWT.BORDER); + mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mPackageText.addModifyListener(this); + mPackageText.addFocusListener(this); + mPackageDec = createFieldDecoration(mPackageText, + "The package name must be a unique identifier for your application. " + + "It is typically not shown to users, but it *must* stay the same " + + "for the lifetime of your application; it is how multiple versions " + + "of the same application are considered the \"same app\". This is " + + "typically the reverse domain name of your organization plus one or " + + "more application identifiers, and it must be a valid Java package " + + "name."); + new Label(container, SWT.NONE); + + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + + Label buildSdkLabel = new Label(container, SWT.NONE); + buildSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1)); + buildSdkLabel.setText("Build SDK:"); + + mBuildSdkCombo = new Combo(container, SWT.READ_ONLY); + mBuildSdkCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + IAndroidTarget[] targets = Sdk.getCurrent().getTargets(); + mMinNameToApi = Maps.newHashMap(); + List<String> labels = new ArrayList<String>(targets.length); + for (IAndroidTarget target : targets) { + String targetLabel = target.getFullName(); + labels.add(targetLabel); + mMinNameToApi.put(targetLabel, target.getVersion().getApiLevel()); + + } + mBuildSdkCombo.setData(targets); + mBuildSdkCombo.setItems(labels.toArray(new String[labels.size()])); + + // Pick most recent platform + List<String> codeNames = Lists.newArrayList(); + int selectIndex = -1; + for (int i = 0, n = targets.length; i < n; i++) { + IAndroidTarget target = targets[i]; + AndroidVersion version = target.getVersion(); + int apiLevel = version.getApiLevel(); + if (version.isPreview()) { + String codeName = version.getCodename(); + String targetLabel = "API " + apiLevel + ":" + codeName; + codeNames.add(targetLabel); + mMinNameToApi.put(targetLabel, apiLevel); + } else if (target.isPlatform() + && (mValues.target == null || + apiLevel > mValues.target.getVersion().getApiLevel())) { + mValues.target = target; + selectIndex = i; + } + } + if (selectIndex != -1) { + mBuildSdkCombo.select(selectIndex); + } + + mBuildSdkCombo.addSelectionListener(this); + mBuildSdkCombo.addFocusListener(this); + mBuildTargetDec = createFieldDecoration(mBuildSdkCombo, + "Choose a target API to compile your code against. This is typically the most " + + "recent version, or the first version that supports all the APIs you want to " + + "directly access"); + + + mChooseSdkButton = new Button(container, SWT.NONE); + mChooseSdkButton.setText("Choose..."); + mChooseSdkButton.addSelectionListener(this); + mChooseSdkButton.setEnabled(false); + + Label minSdkLabel = new Label(container, SWT.NONE); + minSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1)); + minSdkLabel.setText("Minimum Required SDK:"); + + mMinSdkCombo = new Combo(container, SWT.READ_ONLY); + mMinSdkCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + labels = new ArrayList<String>(24); + for (String label : AdtUtils.getKnownVersions()) { + labels.add(label); + } + assert labels.size() >= 15; // *Known* versions to ADT, not installed/available versions + for (String codeName : codeNames) { + labels.add(codeName); + } + String[] versions = labels.toArray(new String[labels.size()]); + mMinSdkCombo.setItems(versions); + if (mValues.target != null && mValues.target.getVersion().isPreview()) { + mValues.minSdk = mValues.target.getVersion().getCodename(); + mMinSdkCombo.setText(mValues.minSdk); + mValues.iconState.minSdk = mValues.target.getVersion().getApiLevel(); + } else { + mMinSdkCombo.select(INITIAL_MIN_SDK - 1); + mValues.minSdk = Integer.toString(INITIAL_MIN_SDK); + mValues.iconState.minSdk = INITIAL_MIN_SDK; + } + mMinSdkCombo.addSelectionListener(this); + mMinSdkCombo.addFocusListener(this); + mMinSdkDec = createFieldDecoration(mMinSdkCombo, + "Choose the lowest version of Android that your application will support. Lower " + + "API levels target more devices, but means fewer features are available. By " + + "targeting API 8 and later, you reach approximately 93% of the market."); + + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + + mCompatToggle = new Button(container, SWT.CHECK); + mCompatToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mCompatToggle.setSelection(true); + mCompatToggle.setEnabled(false); + mCompatToggle.setText("Include compatibility code"); + mCompatToggle.addSelectionListener(this); + new Label(container, SWT.NONE); + + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + + mSkeletonToggle = new Button(container, SWT.CHECK); + mSkeletonToggle.setSelection(true); + mSkeletonToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1)); + mSkeletonToggle.setText("Create Application Skeleton"); + mSkeletonToggle.addSelectionListener(this); + + Label label = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 4, 1)); + + mHelpIcon = new Label(container, SWT.NONE); + mHelpIcon.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1)); + Image icon = IconFactory.getInstance().getIcon("quickfix"); + mHelpIcon.setImage(icon); + mHelpIcon.setVisible(false); + + mTipLabel = new Label(container, SWT.WRAP); + mTipLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1)); + + // Reserve space for 4 lines + mTipLabel.setText("\n\n\n\n"); //$NON-NLS-1$ + + // Reserve enough width to accommodate the various wizard pages up front + // (since they are created lazily, and we don't want the wizard to dynamically + // resize itself for small size adjustments as each successive page is slightly + // larger) + Label dummy = new Label(container, SWT.NONE); + GridData data = new GridData(); + data.horizontalSpan = 4; + data.widthHint = WIZARD_PAGE_WIDTH; + dummy.setLayoutData(data); + + } + + private ControlDecoration createFieldDecoration(Control control, String description) { + ControlDecoration dec = new ControlDecoration(control, SWT.LEFT); + dec.setMarginWidth(2); + FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault(). + getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION); + dec.setImage(errorFieldIndicator.getImage()); + dec.setDescriptionText(description); + control.setToolTipText(description); + + return dec; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + // DURING DEVELOPMENT ONLY + if (assertionsEnabled()) { + String uniqueProjectName = AdtUtils.getUniqueProjectName("Test", ""); + mProjectText.setText(uniqueProjectName); + mPackageText.setText("test.pkg"); + } + + validatePage(); + } + + // ---- Implements ModifyListener ---- + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mProjectText) { + mValues.projectName = mProjectText.getText(); + mValues.projectModified = true; + + try { + mIgnore = true; + if (!mValues.applicationModified) { + mValues.applicationName = mValues.projectName; + mApplicationText.setText(mValues.projectName); + } + updateActivityNames(mValues.projectName); + } finally { + mIgnore = false; + } + suggestPackage(mValues.projectName); + } else if (source == mPackageText) { + mValues.packageName = mPackageText.getText(); + mValues.packageModified = true; + } else if (source == mApplicationText) { + mValues.applicationName = mApplicationText.getText(); + mValues.applicationModified = true; + + try { + mIgnore = true; + if (!mValues.projectModified) { + mValues.projectName = mValues.applicationName; + mProjectText.setText(mValues.applicationName); + } + updateActivityNames(mValues.applicationName); + } finally { + mIgnore = false; + } + suggestPackage(mValues.applicationName); + } + + validatePage(); + } + + private void updateActivityNames(String name) { + try { + mIgnore = true; + if (!mValues.activityNameModified) { + mValues.activityName = extractClassName(name) + ACTIVITY_NAME_SUFFIX; + } + if (!mValues.activityTitleModified) { + mValues.activityTitle = name; + } + } finally { + mIgnore = false; + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mChooseSdkButton) { + // TODO: Open SDK chooser + assert false; + } else if (source == mMinSdkCombo) { + mValues.minSdk = getSelectedMinSdk(); + // If higher than build target, adjust build target + // TODO: implement + + Integer minSdk = mMinNameToApi.get(mValues.minSdk); + if (minSdk != null) { + mValues.iconState.minSdk = minSdk.intValue(); + } else { + assert false : mValues.minSdk; + } + + } else if (source == mBuildSdkCombo) { + mValues.target = getSelectedBuildTarget(); + + // If lower than min sdk target, adjust min sdk target + if (mValues.target.getVersion().isPreview()) { + mValues.minSdk = mValues.target.getVersion().getCodename(); + try { + mIgnore = true; + mMinSdkCombo.setText(mValues.minSdk); + } finally { + mIgnore = false; + } + } else { + String minSdk = mValues.minSdk; + int buildApiLevel = mValues.target.getVersion().getApiLevel(); + if (minSdk != null && !minSdk.isEmpty() + && Character.isDigit(minSdk.charAt(0)) + && buildApiLevel < Integer.parseInt(minSdk)) { + mValues.minSdk = Integer.toString(buildApiLevel); + try { + mIgnore = true; + setSelectedMinSdk(buildApiLevel); + } finally { + mIgnore = false; + } + } + } + } else if (source == mSkeletonToggle) { + mValues.createAppSkeleton = mSkeletonToggle.getSelection(); + mValues.createIcon = mValues.createAppSkeleton; + } + + validatePage(); + } + + private String getSelectedMinSdk() { + // If you're using a preview build, such as android-JellyBean, you have + // to use the codename, e.g. JellyBean, as the minimum SDK as well. + IAndroidTarget buildTarget = getSelectedBuildTarget(); + if (buildTarget != null && buildTarget.getVersion().isPreview()) { + return buildTarget.getVersion().getCodename(); + } + + // +1: First API level (at index 0) is 1 + return Integer.toString(mMinSdkCombo.getSelectionIndex() + 1); + } + + private void setSelectedMinSdk(int api) { + mMinSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1 + } + + @Nullable + private IAndroidTarget getSelectedBuildTarget() { + IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData(); + int index = mBuildSdkCombo.getSelectionIndex(); + if (index >= 0 && index < targets.length) { + return targets[index]; + } else { + return null; + } + } + + private void suggestPackage(String original) { + if (!mValues.packageModified) { + // Create default package name + StringBuilder sb = new StringBuilder(); + sb.append(SAMPLE_PACKAGE_PREFIX); + appendPackage(sb, original); + + mValues.packageName = sb.toString(); + try { + mIgnore = true; + mPackageText.setText(mValues.packageName); + } finally { + mIgnore = false; + } + } + } + + private static void appendPackage(StringBuilder sb, String string) { + for (int i = 0, n = string.length(); i < n; i++) { + char c = string.charAt(i); + if (i == 0 && Character.isJavaIdentifierStart(c) + || i != 0 && Character.isJavaIdentifierPart(c)) { + sb.append(Character.toLowerCase(c)); + } else if ((c == '.' || c == ' ') + && (sb.length() > 0 && sb.charAt(sb.length() - 1) != '.')) { + sb.append('.'); + } else if (c == '-') { + sb.append('_'); + } + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + // ---- Implements FocusListener ---- + + @Override + public void focusGained(FocusEvent e) { + Object source = e.getSource(); + String tip = ""; + if (source == mApplicationText) { + tip = mApplicationDec.getDescriptionText(); + } else if (source == mProjectText) { + tip = mProjectDec.getDescriptionText(); + } else if (source == mBuildSdkCombo) { + tip = mBuildTargetDec.getDescriptionText(); + } else if (source == mMinSdkCombo) { + tip = mMinSdkDec.getDescriptionText(); + } else if (source == mPackageText) { + tip = mPackageDec.getDescriptionText(); + if (mPackageText.getText().startsWith(SAMPLE_PACKAGE_PREFIX)) { + mPackageText.setSelection(0, SAMPLE_PACKAGE_PREFIX.length()); + } + } + mTipLabel.setText(tip); + mHelpIcon.setVisible(tip.length() > 0); + } + + @Override + public void focusLost(FocusEvent e) { + mTipLabel.setText(""); + mHelpIcon.setVisible(false); + } + + // Validation + + private void validatePage() { + IStatus appStatus = validateAppName(); + + IStatus status = appStatus; + + IStatus projectStatus = validateProjectName(); + if (projectStatus != null && (status == null || status.getSeverity() != IStatus.ERROR)) { + status = projectStatus; + } + + IStatus packageStatus = validatePackageName(); + if (packageStatus != null && (status == null || status.getSeverity() != IStatus.ERROR)) { + status = packageStatus; + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.target == null) { + status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "Select an Android build target version"); + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.minSdk == null || mValues.minSdk.isEmpty()) { + status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "Select a minimum SDK version"); + } + } + + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + private IStatus validateAppName() { + String appName = mValues.applicationName; + IStatus status = null; + if (appName == null || appName.isEmpty()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Enter an application name (shown in launcher)"); + } else if (Character.isLowerCase(mValues.applicationName.charAt(0))) { + status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "The application name for most apps begins with an uppercase letter"); + } + + updateDecorator(mApplicationDec, status, true); + + return status; + } + + private IStatus validateProjectName() { + IStatus status = ProjectNamePage.validateProjectName(mValues.projectName); + updateDecorator(mProjectDec, status, true); + + return status; + } + + private IStatus validatePackageName() { + + IStatus status; + if (mValues.packageName == null || mValues.packageName.startsWith(SAMPLE_PACKAGE_PREFIX)) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must be specified."); + } else { + status = ApplicationInfoPage.validatePackage(mValues.packageName); + } + + updateDecorator(mPackageDec, status, true); + + return status; + } + + private void updateDecorator(ControlDecoration decorator, IStatus status, boolean hasInfo) { + if (hasInfo) { + int severity = status != null ? status.getSeverity() : IStatus.OK; + setDecoratorType(decorator, severity); + } else { + if (status == null || status.isOK()) { + decorator.hide(); + } else { + decorator.show(); + } + } + } + + private void setDecoratorType(ControlDecoration decorator, int severity) { + String id; + if (severity == IStatus.ERROR) { + id = FieldDecorationRegistry.DEC_ERROR; + } else if (severity == IStatus.WARNING) { + id = FieldDecorationRegistry.DEC_WARNING; + } else { + id = FieldDecorationRegistry.DEC_INFORMATION; + } + FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault(). + getFieldDecoration(id); + decorator.setImage(errorFieldIndicator.getImage()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java new file mode 100644 index 0000000..60f7a9e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static org.eclipse.core.resources.IResource.DEPTH_INFINITE; + +import com.android.assetstudiolib.GraphicGenerator; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.assetstudio.AssetType; +import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage; +import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator; +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.imageio.ImageIO; + +/** + * Wizard for creating new projects + */ +public class NewProjectWizard extends Wizard implements INewWizard { + private static final String ATTR_COPY_ICONS = "copyIcons"; //$NON-NLS-1$ + static final String ATTR_TARGET_API = "targetApi"; //$NON-NLS-1$ + static final String ATTR_MIN_API = "minApi"; //$NON-NLS-1$ + static final String ATTR_MIN_API_LEVEL = "minApiLevel"; //$NON-NLS-1$ + static final String ATTR_PACKAGE_NAME = "packageName"; //$NON-NLS-1$ + static final String ATTR_APP_TITLE = "appTitle"; //$NON-NLS-1$ + private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$ + + private IWorkbench mWorkbench; + private NewProjectPage mMainPage; + private AppSkeletonPage mAppSkeletonPage; + private NewTemplatePage mTemplatePage; + private ActivityPage mActivityPage; + private ConfigureAssetSetPage mIconPage; + private NewProjectWizardState mValues; + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mWorkbench = workbench; + + setWindowTitle("New Android App"); + setHelpAvailable(false); + setImageDescriptor(); + + mValues = new NewProjectWizardState(); + mMainPage = new NewProjectPage(mValues); + mAppSkeletonPage = new AppSkeletonPage(mValues); + mActivityPage = new ActivityPage(mValues); + } + + /** + * Adds pages to this wizard. + */ + @Override + public void addPages() { + addPage(mMainPage); + addPage(mAppSkeletonPage); + addPage(mActivityPage); + } + + @Override + public IWizardPage getNextPage(IWizardPage page) { + // If you turn off creating an application, only one page + if (page == mMainPage) { + if (!mValues.createAppSkeleton) { + return null; + } + } + + if (page == mActivityPage && mValues.createActivity) { + if (mTemplatePage == null) { + NewTemplateWizardState activityValues = mValues.activityValues; + + // Initialize the *default* activity name based on what we've derived + // from the project name + activityValues.defaults.put("activityName", mValues.activityName); + + // Hide those parameters that the template requires but that we don't want to + // ask the users about, since we will supply these values from the rest + // of the new project wizard. + Set<String> hidden = activityValues.hidden; + hidden.add(ATTR_PACKAGE_NAME); + hidden.add(ATTR_APP_TITLE); + hidden.add(ATTR_MIN_API); + hidden.add(ATTR_MIN_API_LEVEL); + hidden.add(ATTR_TARGET_API); + + mTemplatePage = new NewTemplatePage(activityValues, false); + addPage(mTemplatePage); + } + return mTemplatePage; + } + + if (page == mTemplatePage || !mValues.createActivity && page == mActivityPage) { + if (mValues.createIcon) { + if (mIconPage == null) { + // Bundle asset studio wizard to create the launcher icon + CreateAssetSetWizardState iconState = mValues.iconState; + iconState.type = AssetType.LAUNCHER; + iconState.outputName = "ic_launcher"; //$NON-NLS-1$ + iconState.background = new RGB(0xff, 0xff, 0xff); + iconState.foreground = new RGB(0x33, 0xb6, 0xea); + iconState.shape = GraphicGenerator.Shape.CIRCLE; + iconState.trim = true; + iconState.padding = 10; + iconState.sourceType = CreateAssetSetWizardState.SourceType.CLIPART; + iconState.clipartName = "user.png"; //$NON-NLS-1$ + mIconPage = new ConfigureAssetSetPage(iconState); + mIconPage.setTitle("Configure Launcher Icon"); + addPage(mIconPage); + } + return mIconPage; + } else { + return null; + } + } + + return super.getNextPage(page); + } + + @Override + public boolean canFinish() { + if (!mValues.createAppSkeleton) { + return mMainPage.isPageComplete(); + } + + // Deal with lazy creation of some pages: these may not be in the page-list yet + // since they are constructed lazily, so consider that option here. + if (mValues.createIcon && (mIconPage == null || !mIconPage.isPageComplete())) { + return false; + } + if (mValues.createActivity && (mTemplatePage == null || !mTemplatePage.isPageComplete())) { + return false; + } + + // Override super behavior (which just calls isPageComplete() on each of the pages) + // to special case the template and icon pages since we want to skip them if + // the appropriate flags are not set. + for (IWizardPage page : getPages()) { + if (page == mTemplatePage && !mValues.createActivity) { + continue; + } + if (page == mIconPage && !mValues.createIcon) { + continue; + } + if (!page.isPageComplete()) { + return false; + } + } + + return true; + } + + @Override + public boolean performFinish() { + try { + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + String name = mValues.projectName; + final IProject newProject = root.getProject(name); + + final TemplateHandler template = mValues.template; + // We'll be merging in an activity template, but don't create *~ backup files + // of the merged files (such as the manifest file) in that case. + template.setBackupMergedFiles(false); + + Runnable projectPopulator = new Runnable() { + @Override + public void run() { + // Generate basic output skeleton + Map<String, Object> paramMap = new HashMap<String, Object>(); + paramMap.put(ATTR_PACKAGE_NAME, mValues.packageName); + paramMap.put(ATTR_APP_TITLE, mValues.applicationName); + paramMap.put(ATTR_MIN_API, mValues.minSdk); + paramMap.put(ATTR_MIN_API_LEVEL, mValues.minSdkLevel); + paramMap.put(ATTR_TARGET_API, 15); + paramMap.put(ATTR_COPY_ICONS, !mValues.createIcon); + + File outputPath = AdtUtils.getAbsolutePath(newProject).toFile(); + template.render(outputPath, paramMap); + + if (mValues.createAppSkeleton) { + if (mValues.createIcon) { + generateIcons(newProject); + } + + if (mValues.createActivity) { + generateActivity(template, paramMap, outputPath); + } + } + } + }; + + IProgressMonitor monitor = new NullProgressMonitor(); + NewProjectCreator.create(monitor, newProject, mValues.target, projectPopulator); + + try { + newProject.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + // Open the primary file/files + final List<String> filesToOpen = template.getFilesToOpen(); + NewTemplateWizard.openFiles(newProject, filesToOpen, mWorkbench); + + return true; + } catch (Exception ioe) { + AdtPlugin.log(ioe, null); + return false; + } + } + + /** + * Generate custom icons into the project based on the asset studio wizard state + */ + private void generateIcons(final IProject newProject) { + // Generate the custom icons + assert mValues.createIcon; + Map<String, Map<String, BufferedImage>> categories = + mIconPage.generateImages(false); + for (Map<String, BufferedImage> previews : categories.values()) { + for (Map.Entry<String, BufferedImage> entry : previews.entrySet()) { + String relativePath = entry.getKey(); + IPath dest = new Path(relativePath); + IFile file = newProject.getFile(dest); + + // In case template already created icons (should remove that) + // remove them first + if (file.exists()) { + try { + file.delete(true, new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + NewXmlFileWizard.createWsParentDirectory(file.getParent()); + BufferedImage image = entry.getValue(); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + ImageIO.write(image, "PNG", stream); //$NON-NLS-1$ + byte[] bytes = stream.toByteArray(); + InputStream is = new ByteArrayInputStream(bytes); + file.create(is, true /*force*/, null /*progress*/); + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + try { + file.getParent().refreshLocal(1, new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + } + } + + /** + * Generate the activity: Pre-populate information about the project the + * activity needs but that we don't need to ask about when creating a new + * project + */ + private void generateActivity(final TemplateHandler template, + Map<String, Object> paramMap, File outputPath) { + assert mValues.createActivity; + NewTemplateWizardState activityValues = mValues.activityValues; + Map<String, Object> parameters = activityValues.parameters; + parameters.put(ATTR_PACKAGE_NAME, paramMap.get(ATTR_PACKAGE_NAME)); + parameters.put(ATTR_APP_TITLE, paramMap.get(ATTR_APP_TITLE)); + parameters.put(ATTR_MIN_API, paramMap.get(ATTR_MIN_API)); + parameters.put(ATTR_MIN_API_LEVEL, paramMap.get(ATTR_MIN_API_LEVEL)); + + parameters.put(ATTR_TARGET_API, paramMap.get(ATTR_TARGET_API)); + + TemplateHandler activityTemplate = activityValues.getTemplateHandler(); + activityTemplate.setBackupMergedFiles(false); + activityTemplate.render(outputPath, parameters); + List<String> filesToOpen = activityTemplate.getFilesToOpen(); + template.getFilesToOpen().addAll(filesToOpen); + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java new file mode 100644 index 0000000..48a781f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState; +import com.android.sdklib.IAndroidTarget; + +import java.io.File; + +/** + * Value object which holds the current state of the wizard pages for the + * {@link NewProjectWizard} + */ +public class NewProjectWizardState { + private static final String TEMPLATE_NAME = "NewAndroidApplication"; //$NON-NLS-1$ + + /** Creates a new {@link NewProjectWizardState} */ + public NewProjectWizardState() { + File inputPath = new File(TemplateHandler.getTemplatePath(TEMPLATE_NAME)); + template = TemplateHandler.createFromPath(inputPath); + } + + /** The template handler instantiating the project */ + public final TemplateHandler template; + + /** The name of the project */ + public String projectName; + + /** The derived name of the activity, if any */ + public String activityName; + + /** The derived title of the activity, if any */ + public String activityTitle; + + /** The application name */ + public String applicationName; + + /** The package name */ + public String packageName; + + /** Whether the project name has been edited by the user */ + public boolean projectModified; + + /** Whether the package name has been edited by the user */ + public boolean packageModified; + + /** Whether the activity name has been edited by the user */ + public boolean activityNameModified; + + /** Whether the activity title has been edited by the user */ + public boolean activityTitleModified; + + /** Whether the application name has been edited by the user */ + public boolean applicationModified; + + /** The compilation target to use for this project */ + public IAndroidTarget target; + + /** The minimum SDK API level to use */ + public String minSdk; + + /** The minimum API level, as a string (if the API is a preview release with a codename) */ + public int minSdkLevel; + + /** Whether to create an application skeleton */ + public boolean createAppSkeleton = true; + + /** Whether to create an activity (if so, the activity state is stored in + * {@link #activityValues}) */ + public boolean createActivity = true; + + /** Whether to target phones */ + public boolean phone = true; + + /** Whether to target tablets */ + public boolean tablet = true; + + /** Whether a custom icon should be created instead of just reusing the default (if so, + * the icon wizard state is stored in {@link #iconState}) */ + public boolean createIcon = true; + + // Delegated wizards + + /** State for the asset studio wizard, used to create custom icons */ + public CreateAssetSetWizardState iconState = new CreateAssetSetWizardState(); + + /** State for the template wizard, used to embed an activity template */ + public NewTemplateWizardState activityValues = new NewTemplateWizardState(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java new file mode 100644 index 0000000..a6fdf48 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_ID; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH; + +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; +import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo; +import com.android.tools.lint.detector.api.LintUtils; +import com.google.common.collect.Lists; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +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.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * First wizard page in the "New Project From Template" wizard (which is parameterized + * via template.xml files) + */ +public class NewTemplatePage extends WizardPage + implements ModifyListener, SelectionListener, FocusListener { + /** The default width to use for the wizard page */ + static final int WIZARD_PAGE_WIDTH = 600; + + private final NewTemplateWizardState mValues; + private final boolean mChooseProject; + private boolean mIgnore; + private boolean mShown; + private Control mFirst; + // TODO: Move decorators to the Parameter objects? + private Map<String, ControlDecoration> mDecorations = new HashMap<String, ControlDecoration>(); + private Label mHelpIcon; + private Label mTipLabel; + private ImageControl mPreview; + private Image mPreviewImage; + private ProjectCombo mProjectButton; + private List<Parameter> mParameters; + private StringEvaluator mEvaluator; + + NewTemplatePage(NewTemplateWizardState values, boolean chooseProject) { + super("newTemplatePage"); //$NON-NLS-1$ + mValues = values; + mChooseProject = chooseProject; + } + + @Override + public void createControl(Composite parent2) { + Composite parent = new Composite(parent2, SWT.NULL); + setControl(parent); + GridLayout parentLayout = new GridLayout(3, false); + parentLayout.verticalSpacing = 0; + parentLayout.marginWidth = 0; + parentLayout.marginHeight = 0; + parentLayout.horizontalSpacing = 0; + parent.setLayout(parentLayout); + + // Reserve enough width (since the panel is created lazily later) + Label label = new Label(parent, SWT.NONE); + GridData data = new GridData(); + data.widthHint = WIZARD_PAGE_WIDTH; + label.setLayoutData(data); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + private void onEnter() { + Composite parent = (Composite) getControl(); + + Control[] children = parent.getChildren(); + if (children.length > 0) { + for (Control c : parent.getChildren()) { + c.dispose(); + } + for (ControlDecoration decoration : mDecorations.values()) { + decoration.dispose(); + } + mDecorations.clear(); + } + + Composite container = new Composite(parent, SWT.NULL); + container.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + GridLayout gl_container = new GridLayout(3, false); + gl_container.horizontalSpacing = 10; + container.setLayout(gl_container); + + if (mChooseProject) { + // Project: [button] + String tooltip = "The Android Project where the new resource will be created."; + Label projectLabel = new Label(container, SWT.NONE); + projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + projectLabel.setText("Project:"); + projectLabel.setToolTipText(tooltip); + + ProjectChooserHelper helper = + new ProjectChooserHelper(getShell(), null /* filter */); + mProjectButton = new ProjectCombo(helper, container, mValues.project); + mProjectButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mProjectButton.setToolTipText(tooltip); + mProjectButton.addSelectionListener(this); + + //Label projectSeparator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); + //projectSeparator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1)); + } + + // Add parameters + mFirst = null; + TemplateMetadata template = mValues.getTemplateHandler().getTemplate(); + String thumb = null; + if (template != null) { + thumb = template.getThumbnailPath(); + String title = template.getTitle(); + if (!title.isEmpty()) { + setTitle(title); + } + String description = template.getDescription(); + if (!description.isEmpty()) { + setDescription(description); + } + + Map<String, String> defaults = mValues.defaults; + Set<String> seen = null; + if (LintUtils.assertionsEnabled()) { + seen = new HashSet<String>(); + } + + List<Parameter> parameters = template.getParameters(); + mParameters = new ArrayList<Parameter>(parameters.size()); + for (Parameter parameter : parameters) { + Parameter.Type type = parameter.type; + + if (type == Parameter.Type.SEPARATOR) { + Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1)); + continue; + } + + String id = parameter.id; + assert id != null && !id.isEmpty() : ATTR_ID; + mParameters.add(parameter); + String value = defaults.get(id); + if (value == null) { + value = parameter.initial; + } + + String name = parameter.name; + String help = parameter.help; + + // Required + assert name != null && !name.isEmpty() : ATTR_NAME; + // Ensure id's are unique: + assert seen != null && seen.add(id) : id; + + // Skip attributes that were already provided by the surrounding + // context. For example, when adding into an existing project, + // provide the minimum SDK automatically from the project. + if (mValues.hidden != null && mValues.hidden.contains(id)) { + continue; + } + + if (type == Parameter.Type.STRING) { + // TODO: Look at the constraints to add validators here + // TODO: If I type.equals("layout") add resource validator for layout + // names + // TODO: If I type.equals("class") make class validator + + // TODO: Handle package and id better later + Label label = new Label(container, SWT.NONE); + label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + label.setText(name); + + Text text = new Text(container, SWT.BORDER); + text.setData(parameter); + parameter.control = text; + text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + if (value != null && !value.isEmpty()){ + text.setText(value); + mValues.parameters.put(id, value); + } + + text.addModifyListener(this); + text.addFocusListener(this); + + if (mFirst == null) { + mFirst = text; + } + + if (help != null && !help.isEmpty()) { + text.setToolTipText(help); + ControlDecoration decoration = createFieldDecoration(id, text, help); + } + } else if (type == Parameter.Type.BOOLEAN) { + Label label = new Label(container, SWT.NONE); + label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + + Button checkBox = new Button(container, SWT.CHECK); + checkBox.setText(name); + checkBox.setData(parameter); + parameter.control = checkBox; + checkBox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + if (value != null && !value.isEmpty()){ + Boolean selected = Boolean.valueOf(value); + checkBox.setSelection(selected); + mValues.parameters.put(id, value); + } + + checkBox.addSelectionListener(this); + checkBox.addFocusListener(this); + + if (mFirst == null) { + mFirst = checkBox; + } + + if (help != null && !help.isEmpty()) { + checkBox.setToolTipText(help); + ControlDecoration decoration = createFieldDecoration(id, checkBox, help); + } + + } else if (type == Parameter.Type.ENUM) { + Label label = new Label(container, SWT.NONE); + label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + label.setText(name); + + Combo combo = new Combo(container, SWT.READ_ONLY); + combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + List<Element> options = DomUtilities.getChildren(parameter.element); + assert options.size() > 0; + int selected = 0; + List<String> ids = Lists.newArrayList(); + List<String> labels = Lists.newArrayList(); + for (int i = 0, n = options.size(); i < n; i++) { + Element option = options.get(i); + String optionId = option.getAttribute(ATTR_ID); + assert optionId != null && !optionId.isEmpty() : ATTR_ID; + String isDefault = option.getAttribute(ATTR_DEFAULT); + if (isDefault != null && !isDefault.isEmpty() && + Boolean.valueOf(isDefault)) { + selected = i; + } + NodeList childNodes = option.getChildNodes(); + assert childNodes.getLength() == 1 && + childNodes.item(0).getNodeType() == Node.TEXT_NODE; + String optionLabel = childNodes.item(0).getNodeValue().trim(); + ids.add(optionId); + labels.add(optionLabel); + } + combo.setData(parameter); + parameter.control = combo; + combo.setData(ATTR_ID, ids.toArray(new String[ids.size()])); + assert labels.size() > 0; + combo.setItems(labels.toArray(new String[labels.size()])); + combo.select(selected); + mValues.parameters.put(id, ids.get(selected)); + + combo.addSelectionListener(this); + combo.addFocusListener(this); + + if (mFirst == null) { + mFirst = combo; + } + + if (help != null && !help.isEmpty()) { + combo.setToolTipText(help); + ControlDecoration decoration = createFieldDecoration(id, combo, help); + } + } else { + assert false : type; + } + } + } + + // Preview + mPreview = new ImageControl(parent, SWT.NONE, null); + GridData gd_mImage = new GridData(SWT.CENTER, SWT.CENTER, false, false, 1, 1); + gd_mImage.widthHint = PREVIEW_WIDTH + 2 * PREVIEW_PADDING; + mPreview.setLayoutData(gd_mImage); + + Label separator = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData separatorData = new GridData(SWT.FILL, SWT.TOP, true, false, 3, 1); + separatorData.heightHint = 16; + separator.setLayoutData(separatorData); + + // Generic help + mHelpIcon = new Label(parent, SWT.NONE); + mHelpIcon.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false, 1, 1)); + Image icon = IconFactory.getInstance().getIcon("quickfix"); + mHelpIcon.setImage(icon); + mHelpIcon.setVisible(false); + mTipLabel = new Label(parent, SWT.WRAP); + mTipLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + + if (thumb != null && !thumb.isEmpty()) { + setPreview(thumb); + } + + parent.layout(true, true); + // TODO: This is a workaround for the fact that (at least on OSX) you end up + // with some visual artifacts from the control decorations in the upper left corner + // (outside the parent widget itself) from the initial control decoration placement + // prior to layout. Therefore, perform a redraw. A better solution would be to + // delay creation of the control decorations until layout has been performed. + // Let's do that soon. + parent.getParent().redraw(); + } + + private void setPreview(String thumb) { + if (thumb == null) { + return; + } + + Image oldImage = mPreviewImage; + mPreviewImage = null; + + byte[] data = mValues.getTemplateHandler().readTemplateResource(thumb); + if (data != null) { + try { + mPreviewImage = new Image(getControl().getDisplay(), + new ByteArrayInputStream(data)); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + mPreview.setImage(mPreviewImage); + mPreview.fitToWidth(PREVIEW_WIDTH); + + if (oldImage != null) { + oldImage.dispose(); + } + } + + @Override + public void dispose() { + super.dispose(); + + if (mPreviewImage != null) { + mPreviewImage.dispose(); + mPreviewImage = null; + } + } + + private ControlDecoration createFieldDecoration(String id, Control control, + String description) { + ControlDecoration decoration = new ControlDecoration(control, SWT.LEFT); + decoration.setMarginWidth(2); + FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault(). + getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION); + decoration.setImage(errorFieldIndicator.getImage()); + decoration.setDescriptionText(description); + control.setToolTipText(description); + mDecorations.put(id, decoration); + + return decoration; + } + + @Override + public boolean isPageComplete() { + // Force user to reach this page before hitting Finish + return mShown; + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + onEnter(); + } + + super.setVisible(visible); + + if (mFirst != null) { + mFirst.setFocus(); + } + + if (visible) { + mShown = true; + } + + validatePage(); + } + + /** Returns the parameter associated with the given control */ + @Nullable + private Parameter getParameter(Control control) { + return (Parameter) control.getData(); + } + + // ---- Validation ---- + + private void validatePage() { + IStatus status = null; + + // -- validate project + if (mChooseProject && mValues.project == null) { + status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "Please select an Android project."); + } + + for (Parameter parameter : mParameters) { + IInputValidator validator = parameter.getValidator(mValues.project); + if (validator != null) { + ControlDecoration decoration = mDecorations.get(parameter.id); + String value = parameter.value == null ? "" : parameter.value.toString(); + String error = validator.isValid(value); + if (error != null) { + status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, error); + if (decoration != null) { + updateDecorator(decoration, status, parameter.help); + } + } else if (decoration != null) { + updateDecorator(decoration, null, parameter.help); + } + } + } + + setPageComplete(status == null || status.getSeverity() != IStatus.ERROR); + if (status != null) { + setMessage(status.getMessage(), + status.getSeverity() == IStatus.ERROR + ? IMessageProvider.ERROR : IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + private void updateDecorator(ControlDecoration decorator, IStatus status, String help) { + if (help != null && !help.isEmpty()) { + decorator.setDescriptionText(status != null ? status.getMessage() : help); + + int severity = status != null ? status.getSeverity() : IStatus.OK; + String id; + if (severity == IStatus.ERROR) { + id = FieldDecorationRegistry.DEC_ERROR; + } else if (severity == IStatus.WARNING) { + id = FieldDecorationRegistry.DEC_WARNING; + } else { + id = FieldDecorationRegistry.DEC_INFORMATION; + } + FieldDecoration errorFieldIndicator = FieldDecorationRegistry.getDefault(). + getFieldDecoration(id); + decorator.setImage(errorFieldIndicator.getImage()); + } else { + if (status == null || status.isOK()) { + decorator.hide(); + } else { + decorator.show(); + } + } + } + + // ---- Implements ModifyListener ---- + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source instanceof Text) { + Text text = (Text) source; + editParameter(text, text.getText().trim()); + } + + validatePage(); + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mProjectButton) { + mValues.project = mProjectButton.getSelectedProject(); + } else if (source instanceof Combo) { + Combo combo = (Combo) source; + String[] optionIds = (String[]) combo.getData(ATTR_ID); + int index = combo.getSelectionIndex(); + if (index != -1 && index < optionIds.length) { + String optionId = optionIds[index]; + editParameter(combo, optionId); + TemplateMetadata template = mValues.getTemplateHandler().getTemplate(); + setPreview(template.getThumbnailPath()); + } + } else if (source instanceof Button) { + Button button = (Button) source; + editParameter(button, button.getSelection()); + + TemplateMetadata template = mValues.getTemplateHandler().getTemplate(); + setPreview(template.getThumbnailPath()); + } + + validatePage(); + } + + private void editParameter(Control control, Object value) { + Parameter parameter = getParameter(control); + if (parameter != null) { + String id = parameter.id; + parameter.value = value; + parameter.edited = value != null && !value.toString().isEmpty(); + mValues.parameters.put(id, value); + + // Update dependent variables, if any + for (Parameter p : mParameters) { + if (p == parameter || p.suggest == null || p.edited) { + continue; + } + p.suggest.indexOf(id); + if (!p.suggest.contains(id)) { + continue; + } + + try { + if (mEvaluator == null) { + mEvaluator = new StringEvaluator(); + } + String updated = mEvaluator.evaluate(p.suggest, mParameters); + if (updated != null && !updated.equals(p.value)) { + p.value = updated; + mValues.parameters.put(p.id, updated); + + // Update form widgets + boolean prevIgnore = mIgnore; + try { + mIgnore = true; + if (p.control instanceof Text) { + ((Text) p.control).setText(updated); + } else if (p.control instanceof Button) { + // TODO: Handle + } else if (p.control instanceof Combo) { + // TODO: Handle + } else if (p.control != null) { + assert false : p.control; + } + } finally { + mIgnore = prevIgnore; + } + } + } catch (Throwable t) { + // Pass: Ignore updating if something wrong happens + t.printStackTrace(); // during development only + } + } + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + // ---- Implements FocusListener ---- + + @Override + public void focusGained(FocusEvent e) { + Object source = e.getSource(); + String tip = ""; + + if (source instanceof Control) { + Control control = (Control) source; + Parameter parameter = getParameter(control); + if (parameter != null) { + ControlDecoration decoration = mDecorations.get(parameter.id); + if (decoration != null) { + tip = decoration.getDescriptionText(); + } + } + } + + mTipLabel.setText(tip); + mHelpIcon.setVisible(tip.length() > 0); + } + + @Override + public void focusLost(FocusEvent e) { + mTipLabel.setText(""); + mHelpIcon.setVisible(false); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java new file mode 100644 index 0000000..b7ad998 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API_LEVEL; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_TARGET_API; +import static org.eclipse.core.resources.IResource.DEPTH_INFINITE; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.wizards.newresource.BasicNewResourceWizard; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Template wizard which creates parameterized templates + */ +public class NewTemplateWizard extends Wizard implements INewWizard { + /** Template name and location under /templates in the plugin */ + static final String BLANK_ACTIVITY = "BlankActivity"; //$NON-NLS-1$ + /** Template name and location under /templates in the plugin */ + static final String MASTER_DETAIL_FLOW = "MasterDetailFlow"; //$NON-NLS-1$ + /** Template name and location under /templates in the plugin */ + static final String CUSTOM_VIEW = "CustomView"; //$NON-NLS-1$ + /** Available activity-templates (included in a list in the new project template) */ + static final String[] ACTIVITY_TEMPLATES = + new String[] { BLANK_ACTIVITY, MASTER_DETAIL_FLOW }; + private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$ + + protected IWorkbench mWorkbench; + protected NewTemplatePage mMainPage; + protected NewTemplateWizardState mValues; + private final String mTemplateName; + + NewTemplateWizard(String templateName) { + mTemplateName = templateName; + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mWorkbench = workbench; + + setHelpAvailable(false); + setImageDescriptor(); + + mValues = new NewTemplateWizardState(); + mValues.setTemplateName(mTemplateName); + hideBuiltinParameters(); + + List<IProject> projects = AdtUtils.getSelectedProjects(selection); + if (projects.size() == 1) { + mValues.project = projects.get(0); + } + + mMainPage = new NewTemplatePage(mValues, true); + } + + /** + * Hide those parameters that the template requires but that we don't want + * to ask the users about, since we can derive it from the target project + * the template is written into. + */ + protected void hideBuiltinParameters() { + Set<String> hidden = mValues.hidden; + hidden.add(ATTR_PACKAGE_NAME); + hidden.add(ATTR_MIN_API); + hidden.add(ATTR_MIN_API_LEVEL); + hidden.add(ATTR_TARGET_API); + } + + @Override + public void addPages() { + addPage(mMainPage); + } + + @Override + public boolean performFinish() { + try { + Map<String, Object> parameters = mValues.parameters; + IProject project = mValues.project; + + ManifestInfo manifest = ManifestInfo.get(project); + parameters.put(ATTR_PACKAGE_NAME, manifest.getPackage()); + parameters.put(ATTR_MIN_API, manifest.getMinSdkVersion()); + parameters.put(ATTR_MIN_API_LEVEL, manifest.getMinSdkName()); + parameters.put(ATTR_TARGET_API, manifest.getTargetSdkVersion()); + + File outputPath = AdtUtils.getAbsolutePath(project).toFile(); + TemplateHandler handler = mValues.getTemplateHandler(); + handler.render(outputPath, parameters); + + try { + project.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + List<String> filesToOpen = handler.getFilesToOpen(); + NewTemplateWizard.openFiles(project, filesToOpen, mWorkbench); + + return true; + } catch (Exception ioe) { + AdtPlugin.log(ioe, null); + return false; + } + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + + /** + * Opens the given set of files (as relative paths within a given project + * + * @param project the project containing the paths + * @param relativePaths the paths to files to open + * @param mWorkbench the workbench to open the files in + */ + public static void openFiles( + @NonNull final IProject project, + @NonNull final List<String> relativePaths, + @NonNull final IWorkbench mWorkbench) { + if (!relativePaths.isEmpty()) { + // This has to be delayed in order for focus handling to work correctly + AdtPlugin.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + for (String path : relativePaths) { + IResource resource = project.findMember(path); + if (resource != null) { + if (resource instanceof IFile) { + try { + AdtPlugin.openFile((IFile) resource, null, false); + } catch (PartInitException e) { + AdtPlugin.log(e, "Failed to open %1$s", //$NON-NLS-1$ + resource.getFullPath().toString()); + } + } + boolean isLast = relativePaths.size() == 1 || + path.equals(relativePaths.get(relativePaths.size() - 1)); + if (isLast) { + BasicNewResourceWizard.selectAndReveal(resource, + mWorkbench.getActiveWorkbenchWindow()); + } + } + } + } + }); + } + } + + /** + * Specific New Master Detail Flow wizard + */ + public static class MasterDetailWizard extends NewTemplateWizard { + /** Creates a new {@link MasterDetailWizard} */ + public MasterDetailWizard() { + super(MASTER_DETAIL_FLOW); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + setWindowTitle("New Master Detail Flow"); + super.mMainPage.setTitle("New Master Detail Flow"); + super.mMainPage.setDescription("Creates a new Master Detail Flow"); + } + } + + /** + * Specific New Blank Activity wizard + */ + public static class NewActivityWizard extends NewTemplateWizard { + /** Creates a new {@link NewActivityWizard} */ + public NewActivityWizard() { + super(BLANK_ACTIVITY); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + setWindowTitle("New Blank Activity"); + super.mMainPage.setTitle("New Blank Activity"); + super.mMainPage.setDescription("Creates a new blank activity"); + } + } + + /** + * Specific New Custom View wizard + */ + public static class NewCustomViewWizard extends NewTemplateWizard { + /** Creates a new {@link NewCustomViewWizard} */ + public NewCustomViewWizard() { + super(CUSTOM_VIEW); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + setWindowTitle("New Custom View"); + super.mMainPage.setTitle("New Custom View"); + super.mMainPage.setDescription("Creates a new custom view"); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java new file mode 100644 index 0000000..dc75a71 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard.BLANK_ACTIVITY; + +import com.android.annotations.NonNull; + +import org.eclipse.core.resources.IProject; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Value object which holds the current state of the wizard pages for the + * {@link NewTemplateWizard} + */ +public class NewTemplateWizardState { + /** Name of the template being created */ + private String mTemplateName = BLANK_ACTIVITY; + + /** Template handler responsible for instantiating templates and reading resources */ + private TemplateHandler mTemplateHandler; + + /** Configured parameters, by id */ + public final Map<String, Object> parameters = new HashMap<String, Object>(); + + /** Configured defaults for the parameters, by id */ + public final Map<String, String> defaults = new HashMap<String, String>(); + + /** Ids for parameters which should be hidden (because the client wizard already + * has information for these parameters) */ + public final Set<String> hidden = new HashSet<String>(); + + /** + * The chosen project (which may be null if the wizard page is being + * embedded in the new project wizard) + */ + public IProject project; + + /** Name of the template being created */ + private File mTemplateLocation; + + /** + * Create a new state object for use by the {@link NewTemplatePage} + */ + public NewTemplateWizardState() { + } + + @NonNull + String getTemplateName() { + return mTemplateName; + } + + /** + * Sets the new template name to use + * + * @param templateName the name of the template to use + */ + void setTemplateName(@NonNull String templateName) { + if (!templateName.equals(mTemplateName)) { + mTemplateName = templateName; + mTemplateLocation = null; + mTemplateHandler = null; + } + } + + @NonNull + TemplateHandler getTemplateHandler() { + if (mTemplateHandler == null) { + File inputPath; + if (mTemplateLocation != null) { + inputPath = mTemplateLocation; + } else { + inputPath = new File(TemplateHandler.getTemplatePath(mTemplateName)); + } + mTemplateHandler = TemplateHandler.createFromPath(inputPath); + } + + return mTemplateHandler; + } + + // For template development/testing only + void setTemplateLocation(File file) { + if (!file.equals(mTemplateLocation)) { + mTemplateLocation = file; + mTemplateName = null; + mTemplateHandler = null; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java new file mode 100644 index 0000000..0b8b952 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CONSTRAINTS; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_HELP; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_ID; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SUGGEST; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.google.common.base.Splitter; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.swt.widgets.Control; +import org.w3c.dom.Element; + +import java.util.EnumSet; +import java.util.Locale; + +/** + * A template parameter editable and edited by the user. + * <p> + * Note that this class encapsulates not just the metadata provided by the + * template, but the actual editing operation of that template in the wizard: it + * also captures current values, a reference to the editing widget (such that + * related widgets can be updated when one value depends on another etc) + */ +class Parameter { + enum Type { + STRING, + BOOLEAN, + ENUM, + SEPARATOR; + // TODO: Numbers? + + public static Type get(String name) { + try { + return Type.valueOf(name.toUpperCase(Locale.US)); + } catch (IllegalArgumentException e) { + AdtPlugin.printErrorToConsole("Unexpected template type '" + name + "'"); + AdtPlugin.printErrorToConsole("Expected one of :"); + for (Type s : Type.values()) { + AdtPlugin.printErrorToConsole(" " + s.name().toLowerCase(Locale.US)); + } + } + + return STRING; + } + } + + /** + * Constraints that can be applied to a parameter which helps the UI add a + * validator etc for user input. These are typically combined into a set + * of constraints via an EnumSet. + */ + enum Constraint { + /** + * This value must be unique. This constraint usually only makes sense + * when other constraints are specified, such as {@link #LAYOUT}, which + * means that the parameter should designate a name that does not + * represent an existing layout resource name + */ + UNIQUE, + + /** + * This value must already exist. This constraint usually only makes sense + * when other constraints are specified, such as {@link #LAYOUT}, which + * means that the parameter should designate a name that already exists as + * a resource name. + */ + EXISTS, + + /** The associated value must not be empty */ + NONEMPTY, + + /** The associated value should represent an API level */ + APILEVEL, + + /** The associated value should represent a valid class name */ + CLASS, + + /** The associated value should represent a valid package name */ + PACKAGE, + + /** The associated value should represent a valid layout resource name */ + LAYOUT, + + /** The associated value should represent a valid drawable resource name */ + DRAWABLE, + + /** The associated value should represent a valid id resource name */ + ID, + + /** The associated value should represent a valid string resource name */ + STRING; + + public static Constraint get(String name) { + try { + return Constraint.valueOf(name.toUpperCase(Locale.US)); + } catch (IllegalArgumentException e) { + AdtPlugin.printErrorToConsole("Unexpected template constraint '" + name + "'"); + if (name.indexOf(',') != -1) { + AdtPlugin.printErrorToConsole("Use | to separate constraints"); + } else { + AdtPlugin.printErrorToConsole("Expected one of :"); + for (Constraint s : Constraint.values()) { + AdtPlugin.printErrorToConsole(" " + s.name().toLowerCase(Locale.US)); + } + } + } + + return NONEMPTY; + } + } + + /** The type of parameter */ + @NonNull + public final Type type; + + /** The unique id of the parameter (not displayed to the user) */ + @Nullable + public final String id; + + /** The display name for this parameter */ + @Nullable + public final String name; + + /** + * The initial value for this parameter (see also {@link #suggest} for more + * dynamic defaults + */ + @Nullable + public final String initial; + + /** + * A template expression using other template parameters for producing a + * default value based on other edited parameters, if possible. + */ + @Nullable + public final String suggest; + + /** Help for the parameter, if any */ + @Nullable + public final String help; + + /** The currently edited value */ + @Nullable + public Object value; + + /** The control showing this value */ + @Nullable + public Control control; + + /** The decoration associated with the control */ + @Nullable + public ControlDecoration decoration; + + /** Whether the parameter has been edited */ + public boolean edited; + + /** The element defining this parameter */ + @NonNull + public final Element element; + + /** The constraints applicable for this parameter */ + @NonNull + public final EnumSet<Constraint> constraints; + + /** The validator, if any, for this field */ + private IInputValidator mValidator; + + /** True if this field has no validator */ + private boolean mNoValidator; + + /** Project associated with this validator */ + private IProject mValidatorProject; + + Parameter(@NonNull Element parameter) { + element = parameter; + + String typeName = parameter.getAttribute(TemplateHandler.ATTR_TYPE); + assert typeName != null && !typeName.isEmpty() : TemplateHandler.ATTR_TYPE; + type = Type.get(typeName); + + id = parameter.getAttribute(ATTR_ID); + initial = parameter.getAttribute(ATTR_DEFAULT); + suggest = parameter.getAttribute(ATTR_SUGGEST); + name = parameter.getAttribute(ATTR_NAME); + help = parameter.getAttribute(ATTR_HELP); + String constraintString = parameter.getAttribute(ATTR_CONSTRAINTS); + if (constraintString != null && !constraintString.isEmpty()) { + EnumSet<Constraint> constraintSet = null; + for (String s : Splitter.on('|').omitEmptyStrings().split(constraintString)) { + Constraint constraint = Constraint.get(s); + if (constraintSet == null) { + constraintSet = EnumSet.of(constraint); + } else { + constraintSet = EnumSet.copyOf(constraintSet); + constraintSet.add(constraint); + } + } + constraints = constraintSet; + } else { + constraints = EnumSet.noneOf(Constraint.class); + } + + value = initial; + } + + @Nullable + public IInputValidator getValidator(@Nullable IProject project) { + if (mNoValidator) { + return null; + } + + if (project != mValidatorProject) { + // Force update of validators if the project changes, since the validators + // are often tied to project metadata (for example, the resource name validators + // which look for name conflicts) + mValidator = null; + mValidatorProject = project; + } + + if (mValidator == null) { + if (constraints.contains(Constraint.LAYOUT)) { + if (project != null && constraints.contains(Constraint.UNIQUE)) { + mValidator = ResourceNameValidator.create(false, project, ResourceType.LAYOUT); + } else { + mValidator = ResourceNameValidator.create(false, ResourceFolderType.LAYOUT); + } + return mValidator; + } else if (constraints.contains(Constraint.STRING)) { + if (project != null && constraints.contains(Constraint.UNIQUE)) { + mValidator = ResourceNameValidator.create(false, project, ResourceType.STRING); + } else { + mValidator = ResourceNameValidator.create(false, ResourceFolderType.VALUES); + } + return mValidator; + } else if (constraints.contains(Constraint.ID)) { + if (project != null && constraints.contains(Constraint.UNIQUE)) { + mValidator = ResourceNameValidator.create(false, project, ResourceType.ID); + } else { + mValidator = ResourceNameValidator.create(false, ResourceFolderType.VALUES); + } + return mValidator; + } else if (constraints.contains(Constraint.DRAWABLE)) { + if (project != null && constraints.contains(Constraint.UNIQUE)) { + mValidator = ResourceNameValidator.create(false, project, + ResourceType.DRAWABLE); + } else { + mValidator = ResourceNameValidator.create(false, ResourceFolderType.DRAWABLE); + } + return mValidator; + } else if (constraints.contains(Constraint.CLASS)) { + mValidator = new IInputValidator() { + @Override + public String isValid(String newText) { + IStatus status = ApplicationInfoPage.validateActivity(newText.trim()); + if (status != null && !status.isOK()) { + return status.getMessage(); + } + + return null; + } + }; + return mValidator; + } else if (constraints.contains(Constraint.PACKAGE)) { + mValidator = new IInputValidator() { + @Override + public String isValid(String newText) { + IStatus status = ApplicationInfoPage.validatePackage(newText.trim()); + if (status != null && !status.isOK()) { + return status.getMessage(); + } + + return null; + } + }; + return mValidator; + } else if (constraints.contains(Constraint.NONEMPTY)) { + mValidator = new IInputValidator() { + @Override + public String isValid(String newText) { + if (newText.trim().isEmpty()) { + return String.format("Enter a value for %1$s", name); + } + + return null; + } + }; + return mValidator; + } + + // TODO: Handle EXISTS, APILEVEL (which is currently handled manually in the + // new project wizard, and never actually input by the user in a templated + // wizard) + + mNoValidator = true; + } + + return mValidator; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java new file mode 100644 index 0000000..c1c8073 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; + +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +/** + * A template handler which can evaluate simple strings. Used to evaluate + * parameter constraints during UI wizard value editing. + * <p> + * Unlike the more general {@link TemplateHandler} which is used to instantiate + * full template files (from resources, merging into existing files etc) this + * evaluator supports only simple strings, referencing only values from the + * provided map (and builtin functions). + */ +class StringEvaluator implements TemplateLoader { + private Map<String, Object> mParameters; + private Configuration mFreemarker; + private String mCurrentExpression; + + StringEvaluator() { + mParameters = TemplateHandler.createBuiltinMap(); + + mFreemarker = new Configuration(); + mFreemarker.setObjectWrapper(new DefaultObjectWrapper()); + mFreemarker.setTemplateLoader(this); + } + + /** Evaluates the given expression, with the given set of parameters */ + @Nullable + String evaluate(@NonNull String expression, @NonNull List<Parameter> parameters) { + // Render the instruction list template. + for (Parameter parameter : parameters) { + mParameters.put(parameter.id, parameter.value); + } + try { + mCurrentExpression = expression; + Template inputsTemplate = mFreemarker.getTemplate(expression); + StringWriter out = new StringWriter(); + inputsTemplate.process(mParameters, out); + out.flush(); + return out.toString(); + } catch (Exception e) { + if (assertionsEnabled()) { + AdtPlugin.log(e, null); + } + return null; + } + } + + // ---- Implements TemplateLoader ---- + + @Override + public Object findTemplateSource(String name) throws IOException { + return mCurrentExpression; + } + + @Override + public long getLastModified(Object templateSource) { + return 0; + } + + @Override + public Reader getReader(Object templateSource, String encoding) throws IOException { + return new StringReader(mCurrentExpression); + } + + @Override + public void closeTemplateSource(Object templateSource) throws IOException { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java new file mode 100644 index 0000000..dc0c898 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java @@ -0,0 +1,956 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.AdtConstants.DOT_FTL; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_JAR; +import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; +import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.manifmerger.ManifestMerger; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.SdkConstants; +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; + +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.Template; +import freemarker.template.TemplateException; + +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import lombok.ast.libs.org.parboiled.google.collect.Lists; + +/** + * Handler which manages instantiating FreeMarker templates, copying resources + * and merging into existing files + */ +class TemplateHandler { + /** + * Special marker indicating that this path refers to the special shared + * resource directory rather than being somewhere inside the root/ directory + * where all template specific resources are found + */ + private static final String VALUE_TEMPLATE_DIR = "$TEMPLATEDIR"; //$NON-NLS-1$ + + /** + * Directory within the template which contains the resources referenced + * from the template.xml file + */ + private static final String DATA_ROOT = "root"; //$NON-NLS-1$ + + /** + * Shared resource directory containing common resources shared among + * multiple templates + */ + private static final String RESOURCE_ROOT = "res"; //$NON-NLS-1$ + + /** Relative path within the ADT plugin where the templates are found */ + static final String TEMPLATE_PREFIX = "/templates/"; //$NON-NLS-1$ + + /** Reserved filename which describes each template */ + static final String TEMPLATE_XML = "template.xml"; //$NON-NLS-1$ + + // Various tags and attributes used in the template metadata files - template.xml, + // globals.xml.ftl, recipe.xml.ftl, etc. + + static final String TAG_MERGE = "merge"; //$NON-NLS-1$ + static final String TAG_EXECUTE = "execute"; //$NON-NLS-1$ + static final String TAG_GLOBALS = "globals"; //$NON-NLS-1$ + static final String TAG_GLOBAL = "global"; //$NON-NLS-1$ + static final String TAG_PARAMETER = "parameter"; //$NON-NLS-1$ + static final String TAG_COPY = "copy"; //$NON-NLS-1$ + static final String TAG_INSTANTIATE = "instantiate"; //$NON-NLS-1$ + static final String TAG_OPEN = "open"; //$NON-NLS-1$ + static final String TAG_THUMB = "thumb"; //$NON-NLS-1$ + static final String TAG_THUMBS = "thumbs"; //$NON-NLS-1$ + static final String ATTR_VALUE = "value"; //$NON-NLS-1$ + static final String ATTR_DEFAULT = "default"; //$NON-NLS-1$ + static final String ATTR_SUGGEST = "suggest"; //$NON-NLS-1$ + static final String ATTR_ID = "id"; //$NON-NLS-1$ + static final String ATTR_NAME = "name"; //$NON-NLS-1$ + static final String ATTR_DESCRIPTION = "description";//$NON-NLS-1$ + static final String ATTR_TYPE = "type"; //$NON-NLS-1$ + static final String ATTR_HELP = "help"; //$NON-NLS-1$ + static final String ATTR_FILE = "file"; //$NON-NLS-1$ + static final String ATTR_TO = "to"; //$NON-NLS-1$ + static final String ATTR_FROM = "from"; //$NON-NLS-1$ + static final String ATTR_CONSTRAINTS = "constraints";//$NON-NLS-1$ + + /** Default padding to apply in wizards around the thumbnail preview images */ + static final int PREVIEW_PADDING = 10; + + /** Default width to scale thumbnail preview images in wizards to */ + static final int PREVIEW_WIDTH = 200; + + /** + * List of files to open after the wizard has been created (these are + * identified by {@link #TAG_OPEN} elements in the recipe file + */ + private final List<String> mOpen = Lists.newArrayList(); + + /** Path to the directory containing the templates */ + private final File mRootPath; + + /** The template loader which is responsible for finding (and sharing) template files */ + private final MyTemplateLoader mLoader; + + /** Agree to all file-overwrites from now on? */ + private boolean mYesToAll = false; + + /** Is writing the template cancelled? */ + private boolean mNoToAll = false; + + /** + * Should files that we merge contents into be backed up? If yes, will + * create emacs-style tilde-file backups (filename.xml~) + */ + private boolean mBackupMergedFiles = true; + + /** + * Template metadata + */ + private TemplateMetadata mTemplate; + + /** Creates a new {@link TemplateHandler} for the given root path */ + static TemplateHandler createFromPath(File rootPath) { + return new TemplateHandler(rootPath); + } + + private TemplateHandler(File rootPath) { + mRootPath = rootPath; + mLoader = new MyTemplateLoader(); + mLoader.setPrefix(mRootPath.getPath()); + } + + public void setBackupMergedFiles(boolean backupMergedFiles) { + mBackupMergedFiles = backupMergedFiles; + } + + public void render(final File outputPath, Map<String, Object> args) { + if (!outputPath.exists()) { + outputPath.mkdirs(); + } + + // Render the instruction list template. + Map<String, Object> paramMap = createParameterMap(args); + Configuration freemarker = new Configuration(); + freemarker.setObjectWrapper(new DefaultObjectWrapper()); + freemarker.setTemplateLoader(mLoader); + + processVariables(freemarker, TEMPLATE_XML, paramMap, outputPath); + } + + Map<String, Object> createParameterMap(Map<String, Object> args) { + final Map<String, Object> paramMap = createBuiltinMap(); + + // Wizard parameters supplied by user, specific to this template + paramMap.putAll(args); + + return paramMap; + } + + /** Data model for the templates */ + static Map<String, Object> createBuiltinMap() { + // Create the data model. + final Map<String, Object> paramMap = new HashMap<String, Object>(); + + // Builtin conversion methods + paramMap.put("slashedPackageName", new FmSlashedPackageNameMethod()); //$NON-NLS-1$ + paramMap.put("camelCaseToUnderscore", new FmCamelCaseToUnderscoreMethod()); //$NON-NLS-1$ + paramMap.put("underscoreToCamelCase", new FmUnderscoreToCamelCaseMethod()); //$NON-NLS-1$ + paramMap.put("activityToLayout", new FmActivityToLayoutMethod()); //$NON-NLS-1$ + paramMap.put("layoutToActivity", new FmLayoutToActivityMethod()); //$NON-NLS-1$ + + // This should be handled better: perhaps declared "required packages" as part of the + // inputs? (It would be better if we could conditionally disable template based + // on availability) + Map<String, String> builtin = new HashMap<String, String>(); + builtin.put("templatesRes", VALUE_TEMPLATE_DIR); //$NON-NLS-1$ + paramMap.put("android", builtin); //$NON-NLS-1$ + + return paramMap; + } + + @Nullable + public TemplateMetadata getTemplate() { + if (mTemplate == null) { + String xml = readTemplateTextResource(TEMPLATE_XML); + if (xml != null) { + Document doc = DomUtilities.parseDocument(xml, true); + if (doc != null && doc.getDocumentElement() != null) { + mTemplate = new TemplateMetadata(doc); + } + } + } + + return mTemplate; + } + + @Nullable + public static TemplateMetadata getTemplate(String templateName) { + String relative = getTemplatePath(templateName) + '/' +TEMPLATE_XML; + String xml = AdtPlugin.readEmbeddedTextFile(relative); + Document doc = DomUtilities.parseDocument(xml, true); + if (doc != null && doc.getDocumentElement() != null) { + return new TemplateMetadata(doc); + } + + return null; + } + + @NonNull + public static String getTemplatePath(String templateName) { + return TEMPLATE_PREFIX + templateName; + } + + @NonNull + public String getResourcePath(String templateName) { + return new File(mRootPath.getPath(), templateName).getPath(); + } + + /** + * Load a text resource for the given relative path within the template + * + * @param relativePath relative path within the template + * @return the string contents of the template text file + */ + @Nullable + public String readTemplateTextResource(@NonNull String relativePath) { + if (mRootPath.getPath().startsWith(TEMPLATE_PREFIX)) { + return AdtPlugin.readEmbeddedTextFile(getResourcePath(relativePath)); + } else { + try { + return Files.toString(new File(mRootPath, relativePath), Charsets.UTF_8); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + } + + @Nullable + public String readTemplateTextResource(@NonNull File file) { + if (mRootPath.getPath().startsWith(TEMPLATE_PREFIX)) { + return AdtPlugin.readEmbeddedTextFile(file.getPath()); + } else { + try { + return Files.toString(file, Charsets.UTF_8); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + } + + /** + * Reads the contents of a resource + * + * @param relativePath the path relative to the template directory + * @return the binary data read from the file + */ + @Nullable + public byte[] readTemplateResource(@NonNull String relativePath) { + if (mRootPath.getPath().startsWith(TEMPLATE_PREFIX)) { + return AdtPlugin.readEmbeddedFile(getResourcePath(relativePath)); + } else { + try { + return Files.toByteArray(new File(mRootPath, relativePath)); + } catch (IOException e) { + AdtPlugin.log(e, null); + return null; + } + } + } + + /** Read the given FreeMarker file and process the variable definitions */ + private void processVariables(final Configuration freemarker, + String file, final Map<String, Object> paramMap, final File outputPath) { + try { + String xml; + if (file.endsWith(DOT_XML)) { + // Just read the file + xml = readTemplateTextResource(file); + } else { + mLoader.setTemplateFile(new File(mRootPath, file)); + Template inputsTemplate = freemarker.getTemplate(file); + StringWriter out = new StringWriter(); + inputsTemplate.process(paramMap, out); + out.flush(); + xml = out.toString(); + } + + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + saxParser.parse(new ByteArrayInputStream(xml.getBytes()), new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) + throws SAXException { + if (TAG_PARAMETER.equals(name)) { + String id = attributes.getValue(ATTR_ID); + if (!paramMap.containsKey(id)) { + String value = attributes.getValue(ATTR_DEFAULT); + paramMap.put(id, value); + } + } else if (TAG_GLOBAL.equals(name)) { + String id = attributes.getValue(ATTR_ID); + if (!paramMap.containsKey(id)) { + String value = attributes.getValue(ATTR_VALUE); + paramMap.put(id, value); + } + } else if (TAG_GLOBALS.equals(name)) { + // Handle evaluation of variables + String path = attributes.getValue(ATTR_FILE); + if (path != null) { + processVariables(freemarker, path, paramMap, outputPath); + } // else: <globals> root element + } else if (TAG_EXECUTE.equals(name)) { + String path = attributes.getValue(ATTR_FILE); + if (path != null) { + execute(freemarker, path, paramMap, outputPath); + } + } else if (!name.equals("template") && !name.equals("category") + && !name.equals("option")) { + System.err.println("WARNING: Unknown template directive " + name); + } + } + }); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + private boolean canOverwrite(File file) { + if (file.exists() && !file.isDirectory()) { + // Warn that the file already exists and ask the user what to do + if (!mYesToAll) { + MessageDialog dialog = new MessageDialog(null, "File Already Exists", null, + String.format( + "%1$s already exists.\nWould you like to replace it?", + file.getPath()), + MessageDialog.QUESTION, new String[] { + // Yes will be moved to the end because it's the default + "Yes", "No", "Cancel", "Yes to All" + }, 0); + int result = dialog.open(); + switch (result) { + case 0: + // Yes + break; + case 3: + // Yes to all + mYesToAll = true; + break; + case 1: + // No + return false; + case SWT.DEFAULT: + case 2: + // Cancel + mNoToAll = true; + return false; + } + } + + if (mBackupMergedFiles) { + return makeBackup(file); + } else { + return file.delete(); + } + } + + return true; + } + + /** Executes the given recipe file: copying, merging, instantiating, opening files etc */ + private void execute( + final Configuration freemarker, + String file, + final Map<String, Object> paramMap, + final File outputPath) { + try { + mLoader.setTemplateFile(new File(mRootPath, file)); + Template freemarkerTemplate = freemarker.getTemplate(file); + + StringWriter out = new StringWriter(); + freemarkerTemplate.process(paramMap, out); + out.flush(); + String xml = out.toString(); + + // Parse and execute the resulting instruction list. + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + + saxParser.parse(new ByteArrayInputStream(xml.getBytes()), + new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) + throws SAXException { + if (mNoToAll) { + return; + } + + try { + boolean instantiate = TAG_INSTANTIATE.equals(name); + if (TAG_COPY.equals(name) || instantiate) { + String fromPath = attributes.getValue(ATTR_FROM); + String toPath = attributes.getValue(ATTR_TO); + if (toPath == null || toPath.isEmpty()) { + toPath = attributes.getValue(ATTR_FROM); + toPath = AdtUtils.stripSuffix(toPath, DOT_FTL); + } + File to = new File(outputPath, toPath); + if (instantiate) { + instantiate(freemarker, paramMap, fromPath, to); + } else { + copyBundledResource(fromPath, to); + } + } else if (TAG_MERGE.equals(name)) { + String fromPath = attributes.getValue(ATTR_FROM); + String toPath = attributes.getValue(ATTR_TO); + if (toPath == null || toPath.isEmpty()) { + toPath = attributes.getValue(ATTR_FROM); + toPath = AdtUtils.stripSuffix(toPath, DOT_FTL); + } + // Resources in template.xml are located within root/ + File to = new File(outputPath, toPath); + merge(freemarker, paramMap, fromPath, to); + } else if (name.equals(TAG_OPEN)) { + // The relative path here is within the output directory: + String relativePath = attributes.getValue(ATTR_FILE); + if (relativePath != null && !relativePath.isEmpty()) { + mOpen.add(relativePath); + } + } else if (!name.equals("recipe")) { //$NON-NLS-1$ + System.err.println("WARNING: Unknown template directive " + name); + } + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + }); + + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + private File getFullPath(String fromPath) { + if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) { + return new File(mRootPath.getParentFile(), RESOURCE_ROOT + + fromPath.substring(VALUE_TEMPLATE_DIR.length())); + } + return new File(mRootPath, DATA_ROOT + File.separator + fromPath); + } + + private void merge( + @NonNull final Configuration freemarker, + @NonNull final Map<String, Object> paramMap, + @NonNull String relativeFrom, + @NonNull File to) throws IOException, TemplateException { + if (!to.exists()) { + // The target file doesn't exist: don't merge, just copy + boolean instantiate = relativeFrom.endsWith(DOT_FTL); + if (instantiate) { + instantiate(freemarker, paramMap, relativeFrom, to); + } else { + copyBundledResource(relativeFrom, to); + } + return; + } + + if (!to.getPath().endsWith(DOT_XML)) { + throw new RuntimeException("Only XML files can be merged at this point: " + to); + } + + String xml = null; + File from = getFullPath(relativeFrom); + if (relativeFrom.endsWith(DOT_FTL)) { + // Perform template substitution of the template prior to merging + mLoader.setTemplateFile(from); + Template template = freemarker.getTemplate(from.getName()); + Writer out = new StringWriter(); + template.process(paramMap, out); + out.flush(); + xml = out.toString(); + } else { + xml = readTemplateTextResource(from); + } + + String currentXml = Files.toString(to, Charsets.UTF_8); + Document currentManifest = DomUtilities.parseStructuredDocument(currentXml); + Document fragment = DomUtilities.parseStructuredDocument(xml); + + XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST; + boolean modified; + boolean ok; + if (to.getName().equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { + modified = ok = mergeManifest(currentManifest, fragment); + } else { + // Merge plain XML files + ResourceFolderType folderType = + ResourceFolderType.getFolderType(to.getParentFile().getName()); + if (folderType != null) { + formatStyle = XmlFormatStyle.getForFolderType(folderType); + } else { + formatStyle = XmlFormatStyle.FILE; + } + + modified = mergeResourceFile(currentManifest, fragment, folderType, paramMap); + ok = true; + } + + // Finally write out the merged file (formatting etc) + if (ok) { + if (modified) { + XmlPrettyPrinter printer = new XmlPrettyPrinter( + XmlFormatPreferences.create(), formatStyle, null); + StringBuilder sb = new StringBuilder(2 ); + printer.prettyPrint(-1, currentManifest, null, null, sb, false /*openTagOnly*/); + String contents = sb.toString(); + writeString(to, contents, false); + } + } else { + // Just insert into file along with comment, using the "standard" conflict + // syntax that many tools and editors recognize. + String sep = AdtUtils.getLineSeparator(); + String contents = + "<<<<<<< Original" + sep + + currentXml + sep + + "=======" + sep + + xml + + ">>>>>>> Added" + sep; + writeString(to, contents, false); + } + } + + /** + * Writes the given contents into the given file (unless that file already + * contains the given contents), and if the file exists ask user whether + * the file should be overwritten (unless the user has already answered "Yes to All" + * or "Cancel" (no to all). + */ + private void writeString(File destination, String contents, boolean confirmOverwrite) + throws IOException { + // First make sure that the files aren't identical, in which case we can do + // nothing (and not involve user) + if (!(destination.exists() + && isIdentical(contents.getBytes(Charsets.UTF_8), destination))) { + // And if the file does exist (and is now known to be different), + // ask user whether it should be replaced (canOverwrite will also + // return true if the file doesn't exist) + if (confirmOverwrite) { + if (!canOverwrite(destination)) { + return; + } + } else { + if (destination.exists()) { + if (mBackupMergedFiles) { + makeBackup(destination); + } else { + destination.delete(); + } + } + } + Files.write(contents, destination, Charsets.UTF_8); + } + } + + /** + * Writes the given contents into the given file (unless that file already + * contains the given contents), and if the file exists ask user whether + * the file should be overwritten (unless the user has already answered "Yes to All" + * or "Cancel" (no to all). + */ + private void writeBytes(File destination, byte[] contents, boolean confirmOverwrite) + throws IOException { + // First make sure that the files aren't identical, in which case we can do + // nothing (and not involve user) + if (!(destination.exists() && isIdentical(contents, destination))) { + // And if the file does exist (and is now known to be different), + // ask user whether it should be replaced (canOverwrite will also + // return true if the file doesn't exist) + if (confirmOverwrite) { + if (!canOverwrite(destination)) { + return; + } + } else { + if (destination.exists()) { + if (mBackupMergedFiles) { + makeBackup(destination); + } else { + destination.delete(); + } + } + } + Files.write(contents, destination); + } + } + + /** Merges the given resource file contents into the given resource file + * @param paramMap */ + private boolean mergeResourceFile(Document currentManifest, Document fragment, + ResourceFolderType folderType, Map<String, Object> paramMap) { + boolean modified = false; + + // For layouts for example, I want to *append* inside the root all the + // contents of the new file. + // But for resources for example, I want to combine elements which specify + // the same name or id attribute. + // For elements like manifest files we need to insert stuff at the right + // location in a nested way (activities in the application element etc) + // but that doesn't happen for the other file types. + Element root = fragment.getDocumentElement(); + NodeList children = root.getChildNodes(); + List<Node> nodes = new ArrayList<Node>(children.getLength()); + for (int i = children.getLength() - 1; i >= 0; i--) { + Node child = children.item(i); + nodes.add(child); + root.removeChild(child); + } + + root = currentManifest.getDocumentElement(); + + if (folderType == ResourceFolderType.VALUES) { + // Try to merge items of the same name + Map<String, Node> old = new HashMap<String, Node>(); + NodeList newSiblings = root.getChildNodes(); + for (int i = newSiblings.getLength() - 1; i >= 0; i--) { + Node child = newSiblings.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) child; + String name = getResourceId(element); + if (name != null) { + old.put(name, element); + } + } + } + + for (Node node : nodes) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + String name = getResourceId(element); + Node replace = name != null ? old.get(name) : null; + if (replace != null) { + // There is an existing item with the same id: just replace it + // ACTUALLY -- let's NOT change it. + // Let's say you've used the activity wizard once, and it + // emits some configuration parameter as a resource that + // it depends on, say "padding". Then the user goes and + // tweaks the padding to some other number. + // Now running the wizard a *second* time for some new activity, + // we should NOT go and set the value back to the template's + // default! + //root.replaceChild(node, replace); + + // ... ON THE OTHER HAND... What if it's a parameter class + // (where the template rewrites a common attribute). Here it's + // really confusing if the new parameter is not set. This is + // really an error in the template, since we shouldn't have conflicts + // like that, but we need to do something to help track this down. + AdtPlugin.log(null, + "Warning: Ignoring name conflict in resource file for name %1$s", + name); + } else { + root.appendChild(node); + modified = true; + } + } + } + } else { + // In other file types, such as layouts, just append all the new content + // at the end. + for (Node node : nodes) { + root.appendChild(node); + modified = true; + } + } + return modified; + } + + /** Merges the given manifest fragment into the given manifest file */ + private boolean mergeManifest(Document currentManifest, Document fragment) { + ManifestMerger merger = new ManifestMerger(AdtPlugin.getDefault()); + return currentManifest != null && fragment != null + && merger.process(currentManifest, fragment); + } + + /** + * Makes a backup of the given file, if it exists, by renaming it to name~ + * (and removing an old name~ file if it exists) + */ + private static boolean makeBackup(File file) { + if (!file.exists()) { + return true; + } + if (file.isDirectory()) { + return false; + } + + File backupFile = new File(file.getParentFile(), file.getName() + '~'); + if (backupFile.exists()) { + backupFile.delete(); + } + return file.renameTo(backupFile); + } + + private static String getResourceId(Element element) { + String name = element.getAttribute(ATTR_NAME); + if (name == null) { + name = element.getAttribute(ATTR_ID); + } + + return name; + } + + /** Instantiates the given template file into the given output file */ + private void instantiate( + @NonNull final Configuration freemarker, + @NonNull final Map<String, Object> paramMap, + @NonNull String relativeFrom, + @NonNull File to) throws IOException, TemplateException { + File parentFile = to.getParentFile(); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + + // For now, treat extension-less files as directories... this isn't quite right + // so I should refine this! Maybe with a unique attribute in the template file? + boolean isDirectory = relativeFrom.indexOf('.') == -1; + if (isDirectory) { + // It's a directory + copyBundledResource(relativeFrom, to); + } else { + File from = getFullPath(relativeFrom); + mLoader.setTemplateFile(from); + Template template = freemarker.getTemplate(from.getName()); + Writer out = new StringWriter(1024); + template.process(paramMap, out); + out.flush(); + String contents = out.toString(); + + if (relativeFrom.endsWith(DOT_XML)) { + XmlFormatStyle formatStyle = XmlFormatStyle.getForFile(new Path(to.getPath())); + XmlFormatPreferences prefs = XmlFormatPreferences.create(); + contents = XmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null); + } + + writeString(to, contents, true); + } + } + + /** + * Returns the list of files to open when the template has been created + * + * @return the list of files to open + */ + @NonNull + public List<String> getFilesToOpen() { + return mOpen; + } + + /** Copy a bundled resource (part of the plugin .jar file) into the given file system path */ + private final void copyBundledResource(String relativeFrom, File output) throws IOException { + File from = getFullPath(relativeFrom); + + // Local file copy? (Only used for the template-development wizard) + if (!mRootPath.getPath().startsWith(TEMPLATE_PREFIX)) { + copy(from, output); + return; + } + + String resourcePath = from.getPath(); + CodeSource source = TemplateHandler.class.getProtectionDomain().getCodeSource(); + if (source != null) { + URL location = source.getLocation(); + try { + URI locationUri = location.toURI(); + File locationFile = new File(locationUri); + if (!locationUri.getPath().endsWith(DOT_JAR)) { + // Plain file; e.g. when running out of Eclipse plugin in + // Eclipse; it uses the bin/ folder instead of running out of a jar + File sourceFile = new File(locationFile, resourcePath); + copy(sourceFile, output); + return; + } + + // Copy out of jar file + JarFile jarFile = new JarFile(locationFile); + int chopIndex = resourcePath.length() + 1; + for (final Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) { + final JarEntry entry = e.nextElement(); + if (entry.getName().startsWith(resourcePath)) { + final String filename = entry.getName().substring(chopIndex); + assert entry.getName().charAt(resourcePath.length()) == '/'; + final File file = new File(output, filename); + if (!entry.isDirectory()) { + // Copy stream + InputStream in = jarFile.getInputStream(entry); + try { + byte[] data = ByteStreams.toByteArray(in); + writeBytes(output, data, true); + } finally { + in.close(); + } + } else { + // Create directory + if (!file.exists() && !file.mkdirs()) { + throw new IOException("Could not create directory " + file); + } + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + } + + /** Returns true if the given file contains the given bytes */ + private static boolean isIdentical(@Nullable byte[] data, @NonNull File dest) + throws IOException { + assert dest.isFile(); + byte[] existing = Files.toByteArray(dest); + return Arrays.equals(existing, data); + } + + /** + * Copies the given source file into the given destination file (where the + * source is allowed to be a directory, in which case the whole directory is + * copied recursively) + */ + private void copy(File src, File dest) throws IOException { + if (src.isDirectory()){ + if (!dest.exists() && !dest.mkdirs()) { + throw new IOException("Could not create directory " + dest); + } + File[] children = src.listFiles(); + if (children != null) { + for (File child : children) { + copy(child, new File(dest, child.getName())); + } + } + } else { + if (dest.exists() && isIdentical(Files.toByteArray(src), dest)) { + return; + } + if (!canOverwrite(dest)) { + return; + } + + File parent = dest.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + Files.copy(src, dest); + } + } + + /** + * A custom {@link TemplateLoader} which locates and provides templates + * within the plugin .jar file + */ + private static final class MyTemplateLoader implements TemplateLoader { + private String mPrefix; + + public void setPrefix(String prefix) { + mPrefix = prefix; + } + + public void setTemplateFile(File file) { + setTemplateParent(file.getParentFile()); + } + + public void setTemplateParent(File parent) { + mPrefix = parent.getPath(); + } + + @Override + public Reader getReader(Object templateSource, String encoding) throws IOException { + URL url = (URL) templateSource; + return new InputStreamReader(url.openStream(), encoding); + } + + @Override + public long getLastModified(Object templateSource) { + return 0; + } + + @Override + public Object findTemplateSource(String name) throws IOException { + String path = mPrefix != null ? mPrefix + '/' + name : name; + URL resource = TemplateHandler.class.getResource(path); + + // Support for local files during template development + if (resource == null && mPrefix != null && !mPrefix.startsWith(TEMPLATE_PREFIX)) { + File file = new File(path); + if (file.exists()) { + return file.toURI().toURL(); + } + } + + return resource; + } + + @Override + public void closeTemplateSource(Object templateSource) throws IOException { + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java new file mode 100644 index 0000000..eac818a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DESCRIPTION; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_NAME; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_PARAMETER; +import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_THUMB; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** An ADT template along with metadata */ +class TemplateMetadata { + private final Document mDocument; + private final List<Parameter> mParameters; + private final Map<String, Parameter> mParameterMap; + + TemplateMetadata(@NonNull Document document) { + mDocument = document; + + NodeList parameters = mDocument.getElementsByTagName(TAG_PARAMETER); + mParameters = new ArrayList<Parameter>(parameters.getLength()); + mParameterMap = new HashMap<String, Parameter>(parameters.getLength()); + for (int index = 0, max = parameters.getLength(); index < max; index++) { + Element element = (Element) parameters.item(index); + Parameter parameter = new Parameter(element); + mParameters.add(parameter); + if (parameter.id != null) { + mParameterMap.put(parameter.id, parameter); + } + } + } + + @Nullable + String getTitle() { + String name = mDocument.getDocumentElement().getAttribute(ATTR_NAME); + if (name != null && !name.isEmpty()) { + return name; + } + + return null; + } + + @Nullable + String getDescription() { + String description = mDocument.getDocumentElement().getAttribute(ATTR_DESCRIPTION); + if (description != null && !description.isEmpty()) { + return description; + } + + return null; + } + + @Nullable + String getThumbnailPath() { + // Apply selector logic. Pick the thumb first thumb that satisfies the largest number + // of conditions. + NodeList thumbs = mDocument.getElementsByTagName(TAG_THUMB); + if (thumbs.getLength() == 0) { + return null; + } + + + int bestMatchCount = 0; + Element bestMatch = null; + + for (int i = 0, n = thumbs.getLength(); i < n; i++) { + Element thumb = (Element) thumbs.item(i); + + NamedNodeMap attributes = thumb.getAttributes(); + if (bestMatch == null && attributes.getLength() == 0) { + bestMatch = thumb; + } else if (attributes.getLength() <= bestMatchCount) { + // Already have a match with this number of attributes, no point checking + continue; + } else { + boolean match = true; + for (int j = 0, max = attributes.getLength(); j < max; j++) { + Attr attribute = (Attr) attributes.item(j); + Parameter parameter = mParameterMap.get(attribute.getName()); + if (parameter == null) { + AdtPlugin.log(null, "Unexpected parameter in template thumbnail: %1$s", + attribute.getName()); + continue; + } + String thumbNailValue = attribute.getValue(); + String editedValue = parameter.value != null ? parameter.value.toString() : ""; + if (!thumbNailValue.equals(editedValue)) { + match = false; + break; + } + } + if (match) { + bestMatch = thumb; + bestMatchCount = attributes.getLength(); + } + } + } + + if (bestMatch != null) { + NodeList children = bestMatch.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + return child.getNodeValue().trim(); + } + } + } + + return null; + } + + /** Returns the list of available parameters */ + @NonNull + List<Parameter> getParameters() { + return mParameters; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java new file mode 100644 index 0000000..90aa28a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** For template developers: Test local template directory */ +public class TemplateTestPage extends WizardPage + implements SelectionListener, ModifyListener { + private Text mLocation; + private Button mButton; + private static String sLocation; // Persist between repeated invocations + private Button mProjectToggle; + private File mTemplate; + + TemplateTestPage() { + super("testWizardPage"); //$NON-NLS-1$ + setTitle("Wizard Tester"); + setDescription("Test a new template"); + } + + @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + container.setLayout(new GridLayout(3, false)); + + Label label = new Label(container, SWT.NONE); + label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + label.setText("Template Location:"); + + mLocation = new Text(container, SWT.BORDER); + GridData gd_mLocation = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); + gd_mLocation.widthHint = 400; + mLocation.setLayoutData(gd_mLocation); + if (sLocation != null) { + mLocation.setText(sLocation); + } + mLocation.addModifyListener(this); + + mButton = new Button(container, SWT.FLAT); + mButton.setText("..."); + + mProjectToggle = new Button(container, SWT.CHECK); + mProjectToggle.setEnabled(false); + mProjectToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mProjectToggle.setText("Full project template"); + new Label(container, SWT.NONE); + mButton.addSelectionListener(this); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + validatePage(); + } + + private boolean validatePage() { + String error = null; + + String path = mLocation.getText().trim(); + if (path == null || path.length() == 0) { + error = "Select a template directory"; + mTemplate = null; + } else { + mTemplate = new File(path); + if (!mTemplate.exists()) { + error = String.format("%1$s does not exist", path); + } else { + // Preserve across wizard sessions + sLocation = path; + + if (mTemplate.isDirectory()) { + if (!new File(mTemplate, TemplateHandler.TEMPLATE_XML).exists()) { + error = String.format("Not a template: missing template.xml file in %1$s ", + path); + } + } else { + if (mTemplate.getName().equals(TemplateHandler.TEMPLATE_XML)) { + mTemplate = mTemplate.getParentFile(); + } else { + error = String.format("Select a directory containing a template"); + } + } + } + } + + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else { + setErrorMessage(null); + setMessage(null); + } + + return error == null; + } + + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == mButton) { + DirectoryDialog dialog = new DirectoryDialog(mButton.getShell(), SWT.OPEN); + String path = mLocation.getText().trim(); + if (path.length() > 0) { + dialog.setFilterPath(path); + } + String file = dialog.open(); + if (file != null) { + mLocation.setText(file); + } + } + + validatePage(); + } + + + + File getLocation() { + return mTemplate; + } + + boolean isProjectTemplate() { + return mProjectToggle.getSelection(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java new file mode 100644 index 0000000..bca50c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.templates; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.ui.IWorkbench; + +import java.io.File; + +/** + * Template wizard which creates parameterized templates + */ +public class TemplateTestWizard extends NewTemplateWizard { + private TemplateTestPage mSelectionPage; + private IProject mProject; + + /** Creates a new wizard for testing template definitions in a local directory */ + public TemplateTestWizard() { + super(""); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + if (mValues != null) { + mProject = mValues.project; + } + + mMainPage = null; + mValues = null; + + mSelectionPage = new TemplateTestPage(); + } + + @Override + public void addPages() { + addPage(mSelectionPage); + } + + @Override + public IWizardPage getNextPage(IWizardPage page) { + if (page == mSelectionPage) { + File file = mSelectionPage.getLocation(); + if (file != null && file.exists()) { + if (mValues == null) { + mValues = new NewTemplateWizardState(); + mValues.setTemplateLocation(file); + mValues.project = mProject; + hideBuiltinParameters(); + + mMainPage = new NewTemplatePage(mValues, true); + addPage(mMainPage); + } + + return mMainPage; + } + } + + return super.getNextPage(page); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/globals.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/globals.xml.ftl new file mode 100644 index 0000000..3a26abd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/globals.xml.ftl @@ -0,0 +1,5 @@ +<?xml version="1.0"?> +<globals> + <global id="srcOut" value="src/${slashedPackageName(packageName)}" /> + <global id="menuName" value="${layoutName}" /> +</globals> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/recipe.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/recipe.xml.ftl new file mode 100644 index 0000000..2ce72db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/recipe.xml.ftl @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<recipe> + <merge from="AndroidManifest.xml.ftl" /> + + <copy from="res/drawable-hdpi" /> + <copy from="res/drawable-mdpi" /> + <copy from="res/drawable-xhdpi" /> + + <copy from="res/menu/main.xml" + to="res/menu/${menuName}.xml" /> + + <merge from="res/values/dimens.xml" /> + <merge from="res/values-large/dimens.xml" /> + <merge from="res/values/strings.xml.ftl" /> + + <!-- Decide whether or not to add the support library --> + <#if navType != "none"> + <copy from="${android.templatesRes}/android-support-v4.jar.bin" + to="libs/android-support-v4.jar" /> + </#if> + + <!-- Decide what kind of layout to add (viewpager or not) --> + <#if navType?contains("pager")> + <instantiate from="res/layout/activity_pager.xml.ftl" + to="res/layout/${layoutName}.xml" /> + + <#elseif navType == "tabs" || navType == "dropdown"> + <instantiate from="res/layout/activity_fragment_container.xml" + to="res/layout/${layoutName}.xml" /> + + <#else> + <instantiate from="res/layout/activity_simple.xml" + to="res/layout/${layoutName}.xml" /> + </#if> + + <!-- Decide which activity code to add --> + <#if navType == "none"> + <instantiate from="src/app_package/SimpleActivity.java.ftl" + to="${srcOut}/${activityClass}.java" /> + + <#elseif navType == "tabs_pager" || navType == "pager_strip"> + <instantiate from="src/app_package/TabsAndPagerActivity.java.ftl" + to="${srcOut}/${activityClass}.java" /> + + <#elseif navType == "tabs"> + <instantiate from="src/app_package/TabsActivity.java.ftl" + to="${srcOut}/${activityClass}.java" /> + + <#elseif navType == "dropdown"> + <instantiate from="src/app_package/DropdownActivity.java.ftl" + to="${srcOut}/${activityClass}.java" /> + + </#if> + + <open file="res/layout/${layoutName}.xml" /> +</recipe> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/AndroidManifest.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/AndroidManifest.xml.ftl new file mode 100644 index 0000000..ffcce79 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/AndroidManifest.xml.ftl @@ -0,0 +1,15 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" > + + <application> + + <activity android:name=".${activityClass}" + android:label="@string/activity_name"> + <intent-filter android:label="@string/activity_name"> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> + +</manifest> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-hdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-hdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..67de12d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-hdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-mdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-mdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..134d549 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-mdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-xhdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-xhdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..d699c6b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/drawable-xhdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_fragment_container.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_fragment_container.xml new file mode 100644 index 0000000..3128b5f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_fragment_container.xml @@ -0,0 +1,6 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".${activityClass}" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_pager.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_pager.xml.ftl new file mode 100644 index 0000000..c8f1604 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_pager.xml.ftl @@ -0,0 +1,22 @@ +<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".${activityClass}"<#if navType != "pager_strip"> /><#else>> + + <!-- + This title strip will display the currently visible page title, as well as the page + titles for adjacent pages. + --> + <android.support.v4.view.PagerTitleStrip android:id="@+id/pager_title_strip" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:background="#33b5e5" + android:textColor="#fff" + android:paddingTop="4dp" + android:paddingBottom="4dp" /> + +</android.support.v4.view.ViewPager> +</#if> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_simple.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_simple.xml new file mode 100644 index 0000000..aa34ee3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/layout/activity_simple.xml @@ -0,0 +1,8 @@ +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:text="@string/hello_world" + android:padding="@dimen/padding_medium" + tools:context=".${activityClass}" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/menu/main.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/menu/main.xml new file mode 100644 index 0000000..cfc10fd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/menu/main.xml @@ -0,0 +1,6 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_settings" + android:title="@string/menu_settings" + android:orderInCategory="100" + android:showAsAction="never" /> +</menu> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values-large/dimens.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values-large/dimens.xml new file mode 100644 index 0000000..d8cd7c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values-large/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <dimen name="padding_small">8dp</dimen> + <dimen name="padding_medium">16dp</dimen> + <dimen name="padding_large">16dp</dimen> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/dimens.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/dimens.xml new file mode 100644 index 0000000..d95a70f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <dimen name="padding_small">8dp</dimen> + <dimen name="padding_medium">8dp</dimen> + <dimen name="padding_large">16dp</dimen> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/strings.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/strings.xml.ftl new file mode 100644 index 0000000..753649d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/res/values/strings.xml.ftl @@ -0,0 +1,13 @@ +<resources> + <string name="activity_name">${appTitle}</string> + + <string name="menu_settings">Settings</string> + + <string name="hello_world">Hello world!</string> + + <#if navType != "none"> + <string name="title_section1">Section 1</string> + <string name="title_section2">Section 2</string> + <string name="title_section3">Section 3</string> + </#if> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/DropdownActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/DropdownActivity.java.ftl new file mode 100644 index 0000000..98c1a8f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/DropdownActivity.java.ftl @@ -0,0 +1,98 @@ +package ${packageName}; + +import android.app.ActionBar; +import android.app.FragmentTransaction; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +public class ${activityClass} extends FragmentActivity implements ActionBar.OnNavigationListener { + + private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item"; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.${layoutName}); + + // Set up the action bar. + final ActionBar actionBar = getActionBar(); + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + + // Set up the dropdown list navigation in the action bar. + actionBar.setListNavigationCallbacks( + // Specify a SpinnerAdapter to populate the dropdown list. + new ArrayAdapter( + actionBar.getThemedContext(), + android.R.layout.simple_list_item_1, + android.R.id.text1, + new String[]{ + getString(R.string.title_section1), + getString(R.string.title_section2), + getString(R.string.title_section3), + }), + this); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM)) { + getActionBar().setSelectedNavigationItem( + savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM)); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, + getActionBar().getSelectedNavigationIndex()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.${menuName}, menu); + return true; + } + + @Override + public boolean onNavigationItemSelected(int position, long id) { + // When the given tab is selected, show the tab contents in the container + Fragment fragment = new DummySectionFragment(); + Bundle args = new Bundle(); + args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1); + fragment.setArguments(args); + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, fragment) + .commit(); + return true; + } + + /** + * A dummy fragment representing a section of the app, but that simply displays dummy text. + */ + public static class DummySectionFragment extends Fragment { + public DummySectionFragment() { + } + + public static final String ARG_SECTION_NUMBER = "section_number"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + TextView textView = new TextView(getActivity()); + textView.setGravity(Gravity.CENTER); + Bundle args = getArguments(); + textView.setText(Integer.toString(args.getInt(ARG_SECTION_NUMBER))); + return textView; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/SimpleActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/SimpleActivity.java.ftl new file mode 100644 index 0000000..1ebc0fa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/SimpleActivity.java.ftl @@ -0,0 +1,19 @@ +package ${packageName}; + +import android.os.Bundle; +import android.app.Activity; +import android.view.Menu; + +public class ${activityClass} extends Activity { + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.${layoutName}); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.${menuName}, menu); + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsActivity.java.ftl new file mode 100644 index 0000000..ab11a7f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsActivity.java.ftl @@ -0,0 +1,94 @@ +package ${packageName}; + +import android.app.ActionBar; +import android.app.FragmentTransaction; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class ${activityClass} extends FragmentActivity implements ActionBar.TabListener { + + private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item"; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.${layoutName}); + + // Set up the action bar. + final ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + // For each of the sections in the app, add a tab to the action bar. + actionBar.addTab(actionBar.newTab().setText(R.string.title_section1).setTabListener(this)); + actionBar.addTab(actionBar.newTab().setText(R.string.title_section2).setTabListener(this)); + actionBar.addTab(actionBar.newTab().setText(R.string.title_section3).setTabListener(this)); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM)) { + getActionBar().setSelectedNavigationItem( + savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM)); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, + getActionBar().getSelectedNavigationIndex()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.${menuName}, menu); + return true; + } + + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + } + + @Override + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + // When the given tab is selected, show the tab contents in the container + Fragment fragment = new DummySectionFragment(); + Bundle args = new Bundle(); + args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, tab.getPosition() + 1); + fragment.setArguments(args); + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, fragment) + .commit(); + } + + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + } + + /** + * A dummy fragment representing a section of the app, but that simply displays dummy text. + */ + public static class DummySectionFragment extends Fragment { + public DummySectionFragment() { + } + + public static final String ARG_SECTION_NUMBER = "section_number"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + TextView textView = new TextView(getActivity()); + textView.setGravity(Gravity.CENTER); + Bundle args = getArguments(); + textView.setText(Integer.toString(args.getInt(ARG_SECTION_NUMBER))); + return textView; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsAndPagerActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsAndPagerActivity.java.ftl new file mode 100644 index 0000000..eb47519 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/root/src/app_package/TabsAndPagerActivity.java.ftl @@ -0,0 +1,151 @@ +package ${packageName}; + +import android.app.ActionBar; +import android.app.FragmentTransaction; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class ${activityClass} extends FragmentActivity<#if navType?contains("tabs")> implements ActionBar.TabListener</#if> { + + /** + * The {@link android.support.v4.view.PagerAdapter} that will provide fragments for each of the + * sections. We use a {@link android.support.v4.app.FragmentPagerAdapter} derivative, which will + * keep every loaded fragment in memory. If this becomes too memory intensive, it may be best + * to switch to a {@link android.support.v4.app.FragmentStatePagerAdapter}. + */ + SectionsPagerAdapter mSectionsPagerAdapter; + + /** + * The {@link ViewPager} that will host the section contents. + */ + ViewPager mViewPager; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.${layoutName}); + + // Create the adapter that will return a fragment for each of the three primary sections + // of the app. + mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); + + <#if navType?contains("tabs")> + // Set up the action bar. + final ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + </#if> + + // Set up the ViewPager with the sections adapter. + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mSectionsPagerAdapter); + + <#if navType?contains("tabs")> + // When swiping between different sections, select the corresponding tab. + // We can also use ActionBar.Tab#select() to do this if we have a reference to the + // Tab. + mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + actionBar.setSelectedNavigationItem(position); + } + }); + + // For each of the sections in the app, add a tab to the action bar. + for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) { + // Create a tab with text corresponding to the page title defined by the adapter. + // Also specify this Activity object, which implements the TabListener interface, as the + // listener for when this tab is selected. + actionBar.addTab( + actionBar.newTab() + .setText(mSectionsPagerAdapter.getPageTitle(i)) + .setTabListener(this)); + } + </#if> + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.${menuName}, menu); + return true; + } + <#if navType?contains("tabs")> + @Override + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + } + + @Override + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + // When the given tab is selected, switch to the corresponding page in the ViewPager. + mViewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { + } + </#if> + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to one of the primary + * sections of the app. + */ + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + public SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int i) { + Fragment fragment = new DummySectionFragment(); + Bundle args = new Bundle(); + args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, i + 1); + fragment.setArguments(args); + return fragment; + } + + @Override + public int getCount() { + return 3; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: return getString(R.string.title_section1).toUpperCase(); + case 1: return getString(R.string.title_section2).toUpperCase(); + case 2: return getString(R.string.title_section3).toUpperCase(); + } + return null; + } + } + + /** + * A dummy fragment representing a section of the app, but that simply displays dummy text. + */ + public static class DummySectionFragment extends Fragment { + public DummySectionFragment() { + } + + public static final String ARG_SECTION_NUMBER = "section_number"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + TextView textView = new TextView(getActivity()); + textView.setGravity(Gravity.CENTER); + Bundle args = getArguments(); + textView.setText(Integer.toString(args.getInt(ARG_SECTION_NUMBER))); + return textView; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template.xml new file mode 100644 index 0000000..302e2cc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template.xml @@ -0,0 +1,69 @@ +<?xml version="1.0"?> +<template + name="New Blank Activity" + description="Creates a new blank activity, with optional inner navigation."> + + <category value="Activities" /> + + <parameter + id="activityClass" + name="Activity Name" + type="string" + constraints="class|nonempty" + suggest="${layoutToActivity(layoutName)}" + default="MainActivity" + help="The name of the activity class to create" /> + + <parameter + id="layoutName" + name="Layout Name" + type="string" + constraints="layout|unique" + suggest="${activityToLayout(activityClass)}" + default="main" + help="The name of the layout to create for the activity" /> + + <parameter + id="navType" + name="Navigation Type" + type="enum" + default="none" + help="The type of navigation to use for the activity" > + <option id="none" default="true">None</option> + <option id="tabs">Tabs</option> + <option id="tabs_pager">Tabs + Swipe</option> + <option id="pager_strip">Swipe Views + Title Strip</option> + <option id="dropdown">Dropdown</option> + </parameter> + + <parameter + id="appTitle" + name="Title" + type="string" + constraints="nonempty" + default="My Application" + help="The name of the activity. For launcher activities, the application title." /> + + <parameter + id="packageName" + name="Package name" + type="string" + constraints="package" + default="com.mycompany.myapp" /> + + <!-- 128x128 thumbnails relative to template.xml --> + <thumbs> + <!-- default thumbnail is required --> + <thumb>template_blank_activity.png</thumb> + <!-- attributes act as selectors based on chosen parameters --> + <thumb navType="none">template_blank_activity.png</thumb> + <thumb navType="tabs">template_blank_activity_tabs.png</thumb> + <thumb navType="tabs_pager">template_blank_activity_tabs_pager.png</thumb> + <thumb navType="pager_strip">template_blank_activity_pager.png</thumb> + <thumb navType="dropdown">template_blank_activity_dropdown.png</thumb> + </thumbs> + + <globals file="globals.xml.ftl" /> + <execute file="recipe.xml.ftl" /> + +</template> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity.png Binary files differnew file mode 100644 index 0000000..729dd1c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_dropdown.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_dropdown.png Binary files differnew file mode 100644 index 0000000..09fa2cf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_dropdown.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_pager.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_pager.png Binary files differnew file mode 100644 index 0000000..7cd8e0e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_pager.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs.png Binary files differnew file mode 100644 index 0000000..86a09d6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs_pager.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs_pager.png Binary files differnew file mode 100644 index 0000000..0697a56 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/BlankActivity/template_blank_activity_tabs_pager.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/globals.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/globals.xml.ftl new file mode 100644 index 0000000..d2eeb40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/globals.xml.ftl @@ -0,0 +1,5 @@ +<?xml version="1.0"?> +<globals> + <global id="srcOut" value="src/${slashedPackageName(packageName)}" /> + <global id="view_class" value="${camelCaseToUnderscore(viewClass)}" /> +</globals> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/recipe.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/recipe.xml.ftl new file mode 100644 index 0000000..d152df0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/recipe.xml.ftl @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<recipe> + <merge from="res/values/attrs.xml.ftl" + to="res/values/attrs_${view_class}.xml" /> + <instantiate from="res/layout/sample.xml.ftl" + to="res/layout/sample_${view_class}.xml" /> + + <instantiate from="src/app_package/CustomView.java.ftl" + to="${srcOut}/${viewClass}.java" /> + + <open file="${srcOut}/${viewClass}.java" /> + <open file="res/layout/sample_${view_class}.xml" /> +</recipe> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/layout/sample.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/layout/sample.xml.ftl new file mode 100755 index 0000000..bdd8c8b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/layout/sample.xml.ftl @@ -0,0 +1,18 @@ +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res/${packageName}" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <${packageName}.${viewClass} + android:background="#ccc" + android:layout_width="300dp" + android:layout_height="300dp" + android:paddingLeft="20dp" + android:paddingBottom="40dp" + app:exampleDimension="24sp" + app:exampleColor="#33b5e5" + app:exampleString="Hello, ${viewClass}" + app:exampleDrawable="@android:drawable/ic_menu_add" /> + +</FrameLayout>
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/values/attrs.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/values/attrs.xml.ftl new file mode 100755 index 0000000..89059d2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/res/values/attrs.xml.ftl @@ -0,0 +1,8 @@ +<resources> + <declare-styleable name="${viewClass}"> + <attr name="exampleString" format="string" /> + <attr name="exampleDimension" format="dimension" /> + <attr name="exampleColor" format="color" /> + <attr name="exampleDrawable" format="color|reference" /> + </declare-styleable> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/src/app_package/CustomView.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/src/app_package/CustomView.java.ftl new file mode 100644 index 0000000..e1c7e13 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/root/src/app_package/CustomView.java.ftl @@ -0,0 +1,181 @@ +package ${packageName}; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.View; + +/** + * TODO: document your custom view class. + */ +public class ${viewClass} extends View { + private String mExampleString; // TODO: use a default from R.string... + private int mExampleColor = Color.RED; // TODO: use a default from R.color... + private float mExampleDimension = 0; // TODO: use a default from R.dimen... + private Drawable mExampleDrawable; + + private TextPaint mTextPaint; + private float mTextWidth; + private float mTextHeight; + + public ${viewClass}(Context context) { + super(context); + init(null, 0); + } + + public ${viewClass}(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public ${viewClass}(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs, defStyle); + } + + private void init(AttributeSet attrs, int defStyle) { + // Load attributes + final TypedArray a = getContext().obtainStyledAttributes( + attrs, R.styleable.${viewClass}, defStyle, 0); + + mExampleString = a.getString( + R.styleable.${viewClass}_exampleString); + mExampleColor = a.getColor( + R.styleable.${viewClass}_exampleColor, + mExampleColor); + // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with + // values that should fall on pixel boundaries. + mExampleDimension = a.getDimension( + R.styleable.${viewClass}_exampleDimension, + mExampleDimension); + + if (a.hasValue(R.styleable.${viewClass}_exampleDrawable)) { + mExampleDrawable = a.getDrawable( + R.styleable.${viewClass}_exampleDrawable); + mExampleDrawable.setCallback(this); + } + + a.recycle(); + + // Set up a default TextPaint object + mTextPaint = new TextPaint(); + mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextAlign(Paint.Align.LEFT); + + // Update TextPaint and text measurements from attributes + invalidateTextPaintAndMeasurements(); + } + + private void invalidateTextPaintAndMeasurements() { + mTextPaint.setTextSize(mExampleDimension); + mTextPaint.setColor(mExampleColor); + mTextWidth = mTextPaint.measureText(mExampleString); + + Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); + mTextHeight = fontMetrics.bottom; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // TODO: consider storing these as member variables to reduce + // allocations per draw cycle. + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + + int contentWidth = getWidth() - paddingLeft - paddingRight; + int contentHeight = getHeight() - paddingTop - paddingBottom; + + // Draw the text. + canvas.drawText(mExampleString, + paddingLeft + (contentWidth - mTextWidth) / 2, + paddingTop + (contentHeight + mTextHeight) / 2, + mTextPaint); + + // Draw the example drawable on top of the text. + if (mExampleDrawable != null) { + mExampleDrawable.setBounds(paddingLeft, paddingTop, + paddingLeft + contentWidth, paddingTop + contentHeight); + mExampleDrawable.draw(canvas); + } + } + + /** + * Gets the example string attribute value. + * @return The example string attribute value. + */ + public String getExampleString() { + return mExampleString; + } + + /** + * Sets the view's example string attribute value. In the example view, this string + * is the text to draw. + * @param exampleString The example string attribute value to use. + */ + public void setExampleString(String exampleString) { + mExampleString = exampleString; + invalidateTextPaintAndMeasurements(); + } + + /** + * Gets the example color attribute value. + * @return The example color attribute value. + */ + public int getExampleColor() { + return mExampleColor; + } + + /** + * Sets the view's example color attribute value. In the example view, this color + * is the font color. + * @param exampleColor The example color attribute value to use. + */ + public void setExampleColor(int exampleColor) { + mExampleColor = exampleColor; + invalidateTextPaintAndMeasurements(); + } + + /** + * Gets the example dimension attribute value. + * @return The example dimension attribute value. + */ + public float getExampleDimension() { + return mExampleDimension; + } + + /** + * Sets the view's example dimension attribute value. In the example view, this dimension + * is the font size. + * @param exampleDimension The example dimension attribute value to use. + */ + public void setExampleDimension(float exampleDimension) { + mExampleDimension = exampleDimension; + invalidateTextPaintAndMeasurements(); + } + + /** + * Gets the example drawable attribute value. + * @return The example drawable attribute value. + */ + public Drawable getExampleDrawable() { + return mExampleDrawable; + } + + /** + * Sets the view's example drawable attribute value. In the example view, this drawable is + * drawn above the text. + * @param exampleDrawable The example drawable attribute value to use. + */ + public void setExampleDrawable(Drawable exampleDrawable) { + mExampleDrawable = exampleDrawable; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/template.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/template.xml new file mode 100644 index 0000000..9511566 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/CustomView/template.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<template + name="New Custom View" + description="Creates a new custom view that extends android.view.View, and exposes custom attributes."> + + <category value="UI Components" /> + + <parameter + id="packageName" + name="Package name" + type="string" + constraints="package" + default="com.mycompany.myapp" /> + + <parameter + id="viewClass" + name="View Class" + type="string" + constraints="class|unique" + default="MyView" + help="By convention, should end in 'View'" /> + + <globals file="globals.xml.ftl" /> + <execute file="recipe.xml.ftl" /> + +</template>
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/globals.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/globals.xml.ftl new file mode 100644 index 0000000..519c081 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/globals.xml.ftl @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<globals> + <global id="srcOut" value="src/${slashedPackageName(packageName)}" /> + <global id="CollectionName" value="${objectKind}List" /> + <global id="collection_name" value="${objectKind?lower_case}_list" /> + <global id="DetailName" value="${objectKind}Detail" /> + <global id="detail_name" value="${objectKind?lower_case}_detail" /> +</globals> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/recipe.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/recipe.xml.ftl new file mode 100644 index 0000000..a07635e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/recipe.xml.ftl @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<recipe> + <merge from="AndroidManifest.xml.ftl" /> + + <copy from="${android.templatesRes}/android-support-v4.jar.bin" + to="libs/android-support-v4.jar" /> + + <merge from="res/values-large/refs.xml.ftl" /> + <merge from="res/values-sw600dp/refs.xml.ftl" /> + <merge from="res/values/strings.xml.ftl" /> + + <instantiate from="res/layout/activity_content_detail.xml.ftl" + to="res/layout/activity_${detail_name}.xml" /> + <instantiate from="res/layout/activity_content_list.xml.ftl" + to="res/layout/activity_${collection_name}.xml" /> + <instantiate from="res/layout/activity_content_twopane.xml.ftl" + to="res/layout/activity_${objectKind?lower_case}_twopane.xml" /> + <instantiate from="res/layout/fragment_content_detail.xml.ftl" + to="res/layout/fragment_${detail_name}.xml" /> + + <instantiate from="src/app_package/ContentDetailActivity.java.ftl" + to="${srcOut}/${DetailName}Activity.java" /> + <instantiate from="src/app_package/ContentDetailFragment.java.ftl" + to="${srcOut}/${DetailName}Fragment.java" /> + <instantiate from="src/app_package/ContentListActivity.java.ftl" + to="${srcOut}/${CollectionName}Activity.java" /> + <instantiate from="src/app_package/ContentListFragment.java.ftl" + to="${srcOut}/${CollectionName}Fragment.java" /> + <instantiate from="src/app_package/dummy/DummyContent.java.ftl" + to="${srcOut}/dummy/DummyContent.java" /> +</recipe> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/AndroidManifest.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/AndroidManifest.xml.ftl new file mode 100644 index 0000000..1b9aa76 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/AndroidManifest.xml.ftl @@ -0,0 +1,16 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <application> + <activity android:name=".${CollectionName}Activity" + android:label="@string/title_${collection_name}"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity android:name=".${DetailName}Activity" + android:label="@string/title_${detail_name}" /> + </application> + +</manifest> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl new file mode 100755 index 0000000..1d6d5dc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl @@ -0,0 +1,4 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/${detail_name}_container" + android:layout_width="match_parent" + android:layout_height="match_parent" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl new file mode 100644 index 0000000..788e763 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl @@ -0,0 +1,7 @@ +<fragment xmlns:android="http://schemas.android.com/apk/res/android" + android:name="${packageName}.${CollectionName}Fragment" + android:id="@+id/${collection_name}" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl new file mode 100644 index 0000000..c7a2c75 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl @@ -0,0 +1,21 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:divider="?android:attr/dividerHorizontal" + android:showDividers="middle"> + + <fragment android:name="${packageName}.${CollectionName}Fragment" + android:id="@+id/${collection_name}" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" /> + + <FrameLayout android:id="@+id/${detail_name}_container" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="3" /> + +</LinearLayout> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl new file mode 100644 index 0000000..9b7ca72 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl @@ -0,0 +1,6 @@ +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + style="?android:attr/textAppearanceLarge" + android:id="@+id/${detail_name}" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-large/refs.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-large/refs.xml.ftl new file mode 100644 index 0000000..f3edd90 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-large/refs.xml.ftl @@ -0,0 +1,3 @@ +<resources> + <item type="layout" name="activity_${collection_name}">@layout/activity_${objectKind?lower_case}_twopane</item> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-sw600dp/refs.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-sw600dp/refs.xml.ftl new file mode 100644 index 0000000..f3edd90 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values-sw600dp/refs.xml.ftl @@ -0,0 +1,3 @@ +<resources> + <item type="layout" name="activity_${collection_name}">@layout/activity_${objectKind?lower_case}_twopane</item> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values/strings.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values/strings.xml.ftl new file mode 100644 index 0000000..9b92c7d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/res/values/strings.xml.ftl @@ -0,0 +1,5 @@ +<resources> + <string name="app_name">My ${objectKindPlural} Application</string> + <string name="title_${collection_name}">${objectKindPlural}</string> + <string name="title_${detail_name}">${objectKind} Detail</string> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl new file mode 100644 index 0000000..a7deaf6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl @@ -0,0 +1,39 @@ +package ${packageName}; + +import android.os.Bundle; +import android.content.Intent; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; + +public class ${DetailName}Activity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_${detail_name}); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + Bundle arguments = new Bundle(); + arguments.putString(${DetailName}Fragment.ARG_ITEM_ID, + getIntent().getStringExtra(${DetailName}Fragment.ARG_ITEM_ID)); + ${DetailName}Fragment fragment = new ${DetailName}Fragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction() + .add(R.id.${detail_name}_container, fragment) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + NavUtils.navigateUpTo(this, new Intent(this, ${CollectionName}Activity.class)); + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailFragment.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailFragment.java.ftl new file mode 100644 index 0000000..a0acb1c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentDetailFragment.java.ftl @@ -0,0 +1,38 @@ +package ${packageName}; + +import ${packageName}.dummy.DummyContent; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class ${DetailName}Fragment extends Fragment { + + public static final String ARG_ITEM_ID = "item_id"; + + DummyContent.DummyItem mItem; + + public ${DetailName}Fragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments().containsKey(ARG_ITEM_ID)) { + mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID)); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_${detail_name}, container, false); + if (mItem != null) { + ((TextView) rootView.findViewById(R.id.${detail_name})).setText(mItem.content); + } + return rootView; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl new file mode 100644 index 0000000..4bc5216 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl @@ -0,0 +1,41 @@ +package ${packageName}; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +public class ${CollectionName}Activity extends FragmentActivity + implements ${CollectionName}Fragment.Callbacks { + + private boolean mTwoPane; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_${collection_name}); + + if (findViewById(R.id.${detail_name}_container) != null) { + mTwoPane = true; + ((${CollectionName}Fragment) getSupportFragmentManager() + .findFragmentById(R.id.${collection_name})) + .setActivateOnItemClick(true); + } + } + + @Override + public void onItemSelected(String id) { + if (mTwoPane) { + Bundle arguments = new Bundle(); + arguments.putString(${DetailName}Fragment.ARG_ITEM_ID, id); + ${DetailName}Fragment fragment = new ${DetailName}Fragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction() + .replace(R.id.${detail_name}_container, fragment) + .commit(); + + } else { + Intent detailIntent = new Intent(this, ${DetailName}Activity.class); + detailIntent.putExtra(${DetailName}Fragment.ARG_ITEM_ID, id); + startActivity(detailIntent); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListFragment.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListFragment.java.ftl new file mode 100644 index 0000000..6b4b9a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/ContentListFragment.java.ftl @@ -0,0 +1,97 @@ +package ${packageName}; + +import ${packageName}.dummy.DummyContent; + +import android.R; +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +public class ${CollectionName}Fragment extends ListFragment { + + private static final String STATE_ACTIVATED_POSITION = "activated_position"; + + private Callbacks mCallbacks = sDummyCallbacks; + private int mActivatedPosition = ListView.INVALID_POSITION; + + public interface Callbacks { + + public void onItemSelected(String id); + } + + private static Callbacks sDummyCallbacks = new Callbacks() { + @Override + public void onItemSelected(String id) { + } + }; + + public ${CollectionName}Fragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(getActivity(), + R.layout.simple_list_item_activated_1, + R.id.text1, + DummyContent.ITEMS)); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (savedInstanceState != null && savedInstanceState + .containsKey(STATE_ACTIVATED_POSITION)) { + setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof Callbacks)) { + throw new IllegalStateException("Activity must implement fragment's callbacks."); + } + + mCallbacks = (Callbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = sDummyCallbacks; + } + + @Override + public void onListItemClick(ListView listView, View view, int position, long id) { + super.onListItemClick(listView, view, position, id); + mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mActivatedPosition != ListView.INVALID_POSITION) { + outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); + } + } + + public void setActivateOnItemClick(boolean activateOnItemClick) { + getListView().setChoiceMode(activateOnItemClick + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); + } + + public void setActivatedPosition(int position) { + if (position == ListView.INVALID_POSITION) { + getListView().setItemChecked(mActivatedPosition, false); + } else { + getListView().setItemChecked(position, true); + } + + mActivatedPosition = position; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/dummy/DummyContent.java.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/dummy/DummyContent.java.ftl new file mode 100644 index 0000000..2b05416 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/root/src/app_package/dummy/DummyContent.java.ftl @@ -0,0 +1,39 @@ +package ${packageName}.dummy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DummyContent { + + public static class DummyItem { + + public String id; + public String content; + + public DummyItem(String id, String content) { + this.id = id; + this.content = content; + } + + @Override + public String toString() { + return content; + } + } + + public static List<DummyItem> ITEMS = new ArrayList<DummyItem>(); + public static Map<String, DummyItem> ITEM_MAP = new HashMap<String, DummyItem>(); + + static { + addItem(new DummyItem("1", "Item 1")); + addItem(new DummyItem("2", "Item 2")); + addItem(new DummyItem("3", "Item 3")); + } + + private static void addItem(DummyItem item) { + ITEMS.add(item); + ITEM_MAP.put(item.id, item); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template.xml new file mode 100644 index 0000000..0eed682 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template.xml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<template + name="New Master/Detail Flow" + description="Creates a new master/detail flow, which is two columns on tablets, and one column on smaller screens. This creates a master fragment, detail fragment, and two activities."> + + <thumbs> + <thumb>template_master_detail.png</thumb> + </thumbs> + + <category value="Flows" /> + + <parameter + id="objectKind" + name="Object Kind" + type="string" + constraints="nonempty" + default="Item" + help="Other examples are 'Person', 'Book', etc." /> + + <parameter + id="objectKindPlural" + name="Object Kind Plural" + type="string" + constraints="nonempty" + default="Items" + help="Other examples are 'People', 'Books', etc." /> + + <parameter + id="appTitle" + name="Application title" + type="string" + constraints="nonempty" + default="My Application" /> + + <parameter + id="packageName" + name="Package name" + type="string" + constraints="package" + default="com.mycompany.myapp" /> + + <globals file="globals.xml.ftl" /> + <execute file="recipe.xml.ftl" /> + +</template>
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template_master_detail.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template_master_detail.png Binary files differnew file mode 100644 index 0000000..f9d3f23 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/MasterDetailFlow/template_master_detail.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/globals.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/globals.xml.ftl new file mode 100644 index 0000000..bfc27eb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/globals.xml.ftl @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<globals> + <global id="srcOut" value="src/${slashedPackageName(packageName)}" /> +</globals> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/recipe.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/recipe.xml.ftl new file mode 100644 index 0000000..b343a10 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/recipe.xml.ftl @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<recipe> + <instantiate from="AndroidManifest.xml.ftl" /> + +<#if copyIcons> + <copy from="res/drawable-hdpi" /> + <copy from="res/drawable-mdpi" /> + <copy from="res/drawable-xhdpi" /> +</#if> + <copy from="res/values/dimens.xml" /> + <copy from="res/values/styles.xml" /> + <copy from="res/values-large/dimens.xml" /> + + <instantiate from="res/values/strings.xml.ftl" /> +</recipe> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/AndroidManifest.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/AndroidManifest.xml.ftl new file mode 100644 index 0000000..c97c601 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/AndroidManifest.xml.ftl @@ -0,0 +1,14 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="${packageName}" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="${minApi}" android:targetSdkVersion="${targetApi}" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_launcher" + android:theme="@style/AppTheme"> + + </application> + +</manifest> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..67de12d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100755 index 0000000..fba1ff0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..134d549 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100755 index 0000000..72a445d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_action_search.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_action_search.png Binary files differnew file mode 100644 index 0000000..d699c6b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_action_search.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100755 index 0000000..002e7b0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values-large/dimens.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values-large/dimens.xml new file mode 100644 index 0000000..d8cd7c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values-large/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <dimen name="padding_small">8dp</dimen> + <dimen name="padding_medium">16dp</dimen> + <dimen name="padding_large">16dp</dimen> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/dimens.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/dimens.xml new file mode 100644 index 0000000..d95a70f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <dimen name="padding_small">8dp</dimen> + <dimen name="padding_medium">8dp</dimen> + <dimen name="padding_large">16dp</dimen> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/strings.xml.ftl b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/strings.xml.ftl new file mode 100644 index 0000000..557e5c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/strings.xml.ftl @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">${appTitle}</string> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/styles.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/styles.xml new file mode 100644 index 0000000..5cd7c2f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/root/res/values/styles.xml @@ -0,0 +1,3 @@ +<resources> + <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar" /> +</resources> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template.xml b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template.xml new file mode 100644 index 0000000..84ba6c7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template.xml @@ -0,0 +1,60 @@ +<?xml version="1.0"?> +<template + name="New Android Application" + description="Creates a new Android application with an activity."> + + <thumbs> + <thumb>template_new_project.png</thumb> + </thumbs> + + <category value="Applications" /> + + <parameter + id="packageName" + name="Package name" + type="string" + constraints="package" + default="com.mycompany.myapp" /> + + <parameter + id="appTitle" + name="Application title" + type="string" + constraints="nonempty" + default="My Application" /> + + <parameter + id="minApi" + name="Minimum API level" + type="string" + constraints="apilevel" + default="7" /> + + <!-- + Usually the same as minApi, but when minApi is a code name this will be the corresponding + API level + --> + <parameter + id="minApiLevel" + name="Minimum API level" + type="string" + constraints="apilevel" + default="7" /> + + <parameter + id="targetApi" + name="Target API level" + type="string" + constraints="apilevel" + default="15" /> + + <parameter + id="copyIcons" + name="Include launcher icons" + type="boolean" + default="true" /> + + <globals file="globals.xml.ftl" /> + <execute file="recipe.xml.ftl" /> + +</template> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template_new_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template_new_project.png Binary files differnew file mode 100644 index 0000000..92e8556 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/NewAndroidApplication/template_new_project.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template index 5f4c383..398b79a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template @@ -1,8 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" + tools:context=".ACTIVITY_NAME" > <TextView android:layout_width="fill_parent" diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/res/android-support-v4.jar.bin b/eclipse/plugins/com.android.ide.eclipse.adt/templates/res/android-support-v4.jar.bin Binary files differnew file mode 100644 index 0000000..d006198 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/res/android-support-v4.jar.bin diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml index c1d43ae..f5a809f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml @@ -127,5 +127,11 @@ class="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage" id="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage" name="%page.name.LogCat"/> + <page + category="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage" + class="com.android.ide.eclipse.ddms.preferences.LogCatColorsPage" + id="com.android.ide.eclipse.ddms.preferences.LogCatColorsPage" + name="Colors"> + </page> </extension> </plugin> diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatColorsPage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatColorsPage.java new file mode 100644 index 0000000..675a51c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatColorsPage.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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.ddms.preferences; + +import com.android.ddmuilib.logcat.LogCatPanel; +import com.android.ide.eclipse.ddms.DdmsPlugin; + +import org.eclipse.jface.preference.ColorFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +public class LogCatColorsPage extends FieldEditorPreferencePage + implements IWorkbenchPreferencePage { + public LogCatColorsPage() { + super(GRID); + setPreferenceStore(DdmsPlugin.getDefault().getPreferenceStore()); + } + + @Override + public void init(IWorkbench workbench) { + } + + @Override + protected void createFieldEditors() { + // colors preference for different log levels + ColorFieldEditor cfe = new ColorFieldEditor(LogCatPanel.VERBOSE_COLOR_PREFKEY, + "Verbose Log Message Color", getFieldEditorParent()); + addField(cfe); + + cfe = new ColorFieldEditor(LogCatPanel.DEBUG_COLOR_PREFKEY, "Debug Log Message Color", + getFieldEditorParent()); + addField(cfe); + + cfe = new ColorFieldEditor(LogCatPanel.INFO_COLOR_PREFKEY, "Info Log Message Color", + getFieldEditorParent()); + addField(cfe); + + cfe = new ColorFieldEditor(LogCatPanel.WARN_COLOR_PREFKEY, "Warning Log Message Color", + getFieldEditorParent()); + addField(cfe); + + cfe = new ColorFieldEditor(LogCatPanel.ERROR_COLOR_PREFKEY, "Error Log Message Color", + getFieldEditorParent()); + addField(cfe); + + cfe = new ColorFieldEditor(LogCatPanel.ASSERT_COLOR_PREFKEY, "Assert Log Message Color", + getFieldEditorParent()); + addField(cfe); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java index 9971e18..a0c5450 100644 --- a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java @@ -30,6 +30,9 @@ import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.preference.FontFieldEditor; import org.eclipse.jface.preference.IntegerFieldEditor; import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Label; import org.eclipse.ui.IPerspectiveDescriptor; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; @@ -62,18 +65,27 @@ public class LogCatPreferencePage extends FieldEditorPreferencePage implements Messages.LogCatPreferencePage_MaxMessages, getFieldEditorParent()); addField(mMaxMessages); + createHorizontalSeparator(); + if (InstallDetails.isAdtInstalled()) { createAdtSpecificFieldEditors(); } } + private void createHorizontalSeparator() { + Label l = new Label(getFieldEditorParent(), SWT.SEPARATOR | SWT.HORIZONTAL); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 3; + l.setLayoutData(gd); + } + private void createAdtSpecificFieldEditors() { mSwitchPerspective = new BooleanFieldEditor(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE, Messages.LogCatPreferencePage_Switch_Perspective, getFieldEditorParent()); addField(mSwitchPerspective); IPerspectiveDescriptor[] perspectiveDescriptors = PlatformUI.getWorkbench().getPerspectiveRegistry().getPerspectives(); - String[][] perspectives; + String[][] perspectives = new String[0][0]; if (perspectiveDescriptors.length > 0) { perspectives = new String[perspectiveDescriptors.length][2]; for (int i = 0; i < perspectiveDescriptors.length; i++) { @@ -81,8 +93,6 @@ public class LogCatPreferencePage extends FieldEditorPreferencePage implements perspectives[i][0] = perspective.getLabel(); perspectives[i][1] = perspective.getId(); } - } else { - perspectives = new String[0][0]; } mWhichPerspective = new ComboFieldEditor(PreferenceInitializer.ATTR_PERSPECTIVE_ID, Messages.LogCatPreferencePage_Switch_To, perspectives, getFieldEditorParent()); @@ -90,6 +100,8 @@ public class LogCatPreferencePage extends FieldEditorPreferencePage implements .getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE), getFieldEditorParent()); addField(mWhichPerspective); + createHorizontalSeparator(); + mAutoMonitorLogcat = new BooleanFieldEditor(LogCatMonitor.AUTO_MONITOR_PREFKEY, Messages.LogCatPreferencePage_AutoMonitorLogcat, getFieldEditorParent()); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssistTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssistTest.java index 3f7d20e..c6e60cb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssistTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssistTest.java @@ -25,13 +25,10 @@ import static com.android.sdklib.SdkConstants.FD_RES; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.animator.AnimationContentAssist; -import com.android.ide.eclipse.adt.internal.editors.animator.AnimationEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.color.ColorContentAssist; -import com.android.ide.eclipse.adt.internal.editors.color.ColorEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.drawable.DrawableContentAssist; -import com.android.ide.eclipse.adt.internal.editors.drawable.DrawableEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutContentAssist; -import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestContentAssist; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; @@ -773,34 +770,34 @@ public class AndroidContentAssistTest extends AdtProjectTest { private void checkLayoutCompletion(String name, String caretLocation) throws Exception { IFile file = getLayoutFile(getProject(), name); - IDE.setDefaultEditor(file, LayoutEditorDelegate.LEGACY_EDITOR_ID); + IDE.setDefaultEditor(file, CommonXmlEditor.ID); checkCompletion(name, file, caretLocation, new LayoutContentAssist()); } private void checkColorCompletion(String name, String caretLocation) throws Exception { IFile file = getTestDataFile(getProject(), name, FD_RES + "/" + FD_RES_COLOR + "/" + name); - IDE.setDefaultEditor(file, ColorEditorDelegate.LEGACY_EDITOR_ID); + IDE.setDefaultEditor(file, CommonXmlEditor.ID); checkCompletion(name, file, caretLocation, new ColorContentAssist()); } private void checkAnimCompletion(String name, String caretLocation) throws Exception { IFile file = getTestDataFile(getProject(), name, FD_RES + "/" + FD_RES_ANIM + "/" + name); - IDE.setDefaultEditor(file, AnimationEditorDelegate.LEGACY_EDITOR_ID); + IDE.setDefaultEditor(file, CommonXmlEditor.ID); checkCompletion(name, file, caretLocation, new AnimationContentAssist()); } private void checkAnimatorCompletion(String name, String caretLocation) throws Exception { IFile file = getTestDataFile(getProject(), name, FD_RES + "/" + FD_RES_ANIMATOR + "/" + name); - IDE.setDefaultEditor(file, AnimationEditorDelegate.LEGACY_EDITOR_ID); + IDE.setDefaultEditor(file, CommonXmlEditor.ID); checkCompletion(name, file, caretLocation, new AnimationContentAssist()); } private void checkDrawableCompletion(String name, String caretLocation) throws Exception { IFile file = getTestDataFile(getProject(), name, FD_RES + "/" + FD_RES_DRAWABLE + "/" + name); - IDE.setDefaultEditor(file, DrawableEditorDelegate.LEGACY_EDITOR_ID); + IDE.setDefaultEditor(file, CommonXmlEditor.ID); checkCompletion(name, file, caretLocation, new DrawableContentAssist()); } @@ -851,6 +848,8 @@ public class AndroidContentAssistTest extends AdtProjectTest { proposals = new ICompletionProposal[0]; } + editor.getEditorSite().getPage().closeAllEditors(false); + return proposals; } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/HyperlinksTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/HyperlinksTest.java index 0d42357..a54376d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/HyperlinksTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/HyperlinksTest.java @@ -19,8 +19,6 @@ import static com.android.sdklib.SdkConstants.FD_SOURCES; import com.android.ide.common.resources.ResourceFile; import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; -import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; import com.android.ide.eclipse.adt.internal.editors.Hyperlinks.ResourceLink; import com.android.ide.eclipse.adt.internal.editors.Hyperlinks.XmlResolver; import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest; @@ -180,6 +178,12 @@ public class HyperlinksTest extends AdtProjectTest { "class=\"com.and^roid.eclipse.tests.TestFragment\""); } + public void testNavigate15() throws Exception { + // Check navigating to a theme resource + checkXmlNavigation("navigation1.xml", "res/layout/navigation1.xml", + "?android:attr/alert^DialogStyle"); + } + // Left to test: // onClick handling // class attributes 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 index a816cff..8ddfff5 100644 --- 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 @@ -411,7 +411,6 @@ public class AdtProjectTest extends SdkTestCase { } } - boolean showBeforeWindow = firstDelta >= beforeLines.length - lastDelta; boolean showAfterWindow = firstDelta >= afterLines.length - lastDelta; diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion55.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion55.txt index 17cf984..8f6cd32 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion55.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion55.txt @@ -12,5 +12,5 @@ android:startOffset : Delay in milliseconds before the animation runs, once star android:repeatCount : Defines how many times the animation should repeat. [integer, enum] android:repeatMode : Defines the animation behavior when it reaches the end and the repeat count is greater than 0 or infinite. [enum] android:zAdjustment : Allows for an adjustment of the Z ordering of the content being animated for the duration of the animation. [enum] -android:background : Special background behind animation. [reference, color] +android:background : Special background behind animation. [color, reference] android:detachWallpaper : Special option for window animations: if this window is on top of a wallpaper, don't animate the wallpaper with it. [boolean] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion56.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion56.txt index 837a6cc..5110e73 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion56.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/anim1-expected-completion56.txt @@ -10,5 +10,5 @@ android:startOffset : Delay in milliseconds before the animation runs, once star android:repeatCount : Defines how many times the animation should repeat. [integer, enum] android:repeatMode : Defines the animation behavior when it reaches the end and the repeat count is greater than 0 or infinite. [enum] android:zAdjustment : Allows for an adjustment of the Z ordering of the content being animated for the duration of the animation. [enum] -android:background : Special background behind animation. [reference, color] +android:background : Special background behind animation. [color, reference] android:detachWallpaper : Special option for window animations: if this window is on top of a wallpaper, don't animate the wallpaper with it. [boolean] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/animator1-expected-completion61.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/animator1-expected-completion61.txt index 1ba8a62..b298ac0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/animator1-expected-completion61.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/animator1-expected-completion61.txt @@ -11,7 +11,6 @@ Code completion in animator1.xml for android:interpolator="^@android:anim/bounce @android: @+id/ @anim/ -@animator/ @color/ @drawable/ @id/ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/broken1-expected-completion20.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/broken1-expected-completion20.txt index c5aa1a0..063dc8c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/broken1-expected-completion20.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/broken1-expected-completion20.txt @@ -1,2 +1,2 @@ Code completion in broken1.xml for android:textColorHigh^: -android:textColorHighlight : Color of the text selection highlight. [reference, color] +android:textColorHighlight : Color of the text selection highlight. [color, reference] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion1-expected-completion39.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion1-expected-completion39.txt index ac18c6d..281d7a0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion1-expected-completion39.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion1-expected-completion39.txt @@ -3,15 +3,15 @@ style : A reference to a custom style [reference] android:bufferType : Determines the minimum type that getText() will return. [enum] android:text : Text to display. [string] android:hint : Hint text to display when the text is empty. [string] -android:textColor : Text color. [reference, color] -android:textColorHighlight : Color of the text selection highlight. [reference, color] -android:textColorHint : Color of the hint text. [reference, color] +android:textColor : Text color. [color, reference] +android:textColorHighlight : Color of the text selection highlight. [color, reference] +android:textColorHint : Color of the hint text. [color, reference] android:textAppearance : Base text color, typeface, size, and style. [reference] android:textSize : Size of the text. [dimension] android:textScaleX : Sets the horizontal scaling factor for the text. [float] android:typeface : Typeface (normal, sans, serif, monospace) for the text. [enum] android:textStyle : Style (bold, italic, bolditalic) for the text. [flag] -android:textColorLink : Text color for links. [reference, color] +android:textColorLink : Text color for links. [color, reference] android:cursorVisible : Makes the cursor visible (the default) or invisible. [boolean] android:maxLines : Makes the TextView be at most this many lines tall. [integer] android:maxHeight : Makes the TextView be at most this many pixels tall. [dimension] @@ -48,10 +48,10 @@ android:autoText : If set, specifies that this TextView has a textual input meth android:editable : If set, specifies that this TextView has an input method. * Deprecated: Use inputType instead. [boolean] android:freezesText : If set, the text view will include its current complete text inside of its frozen icicle in addition to meta-data such as the current cursor position. [boolean] android:ellipsize : If set, causes words that are longer than the view is wide to be ellipsized instead of broken in the middle. [enum] -android:drawableTop : The drawable to be drawn above the text. [reference, color] -android:drawableBottom : The drawable to be drawn below the text. [reference, color] -android:drawableLeft : The drawable to be drawn to the left of the text. [reference, color] -android:drawableRight : The drawable to be drawn to the right of the text. [reference, color] +android:drawableTop : The drawable to be drawn above the text. [color, reference] +android:drawableBottom : The drawable to be drawn below the text. [color, reference] +android:drawableLeft : The drawable to be drawn to the left of the text. [color, reference] +android:drawableRight : The drawable to be drawn to the right of the text. [color, reference] android:drawablePadding : The padding between the drawables and the text. [dimension] android:lineSpacingExtra : Extra spacing between lines of text. [dimension] android:lineSpacingMultiplier : Extra spacing between lines of text, as a multiplier. [float] @@ -75,7 +75,7 @@ android:id : Supply an identifier name for this view, to later retrieve it with android:tag : Supply a tag for this view containing a String, to be retrieved later with View.getTag() or searched for with View.findViewWithTag() . [string] android:scrollX : The initial horizontal scroll offset, in pixels. [dimension] android:scrollY : The initial vertical scroll offset, in pixels. [dimension] -android:background : A drawable to use as the background. [reference, color] +android:background : A drawable to use as the background. [color, reference] android:padding : Sets the padding, in pixels, of all four edges. [dimension] android:paddingLeft : Sets the padding, in pixels, of the left edge; see padding. [dimension] android:paddingTop : Sets the padding, in pixels, of the top edge; see padding. [dimension] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion5-expected-completion40.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion5-expected-completion40.txt index 138e225..be3d8d5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion5-expected-completion40.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completion5-expected-completion40.txt @@ -3,15 +3,15 @@ style : A reference to a custom style [reference] android:bufferType : Determines the minimum type that getText() will return. [enum] android:text : Text to display. [string] android:hint : Hint text to display when the text is empty. [string] -android:textColor : Text color. [reference, color] -android:textColorHighlight : Color of the text selection highlight. [reference, color] -android:textColorHint : Color of the hint text. [reference, color] +android:textColor : Text color. [color, reference] +android:textColorHighlight : Color of the text selection highlight. [color, reference] +android:textColorHint : Color of the hint text. [color, reference] android:textAppearance : Base text color, typeface, size, and style. [reference] android:textSize : Size of the text. [dimension] android:textScaleX : Sets the horizontal scaling factor for the text. [float] android:typeface : Typeface (normal, sans, serif, monospace) for the text. [enum] android:textStyle : Style (bold, italic, bolditalic) for the text. [flag] -android:textColorLink : Text color for links. [reference, color] +android:textColorLink : Text color for links. [color, reference] android:cursorVisible : Makes the cursor visible (the default) or invisible. [boolean] android:maxLines : Makes the TextView be at most this many lines tall. [integer] android:maxHeight : Makes the TextView be at most this many pixels tall. [dimension] @@ -48,10 +48,10 @@ android:autoText : If set, specifies that this TextView has a textual input meth android:editable : If set, specifies that this TextView has an input method. * Deprecated: Use inputType instead. [boolean] android:freezesText : If set, the text view will include its current complete text inside of its frozen icicle in addition to meta-data such as the current cursor position. [boolean] android:ellipsize : If set, causes words that are longer than the view is wide to be ellipsized instead of broken in the middle. [enum] -android:drawableTop : The drawable to be drawn above the text. [reference, color] -android:drawableBottom : The drawable to be drawn below the text. [reference, color] -android:drawableLeft : The drawable to be drawn to the left of the text. [reference, color] -android:drawableRight : The drawable to be drawn to the right of the text. [reference, color] +android:drawableTop : The drawable to be drawn above the text. [color, reference] +android:drawableBottom : The drawable to be drawn below the text. [color, reference] +android:drawableLeft : The drawable to be drawn to the left of the text. [color, reference] +android:drawableRight : The drawable to be drawn to the right of the text. [color, reference] android:drawablePadding : The padding between the drawables and the text. [dimension] android:lineSpacingExtra : Extra spacing between lines of text. [dimension] android:lineSpacingMultiplier : Extra spacing between lines of text, as a multiplier. [float] @@ -75,7 +75,7 @@ android:id : Supply an identifier name for this view, to later retrieve it with android:tag : Supply a tag for this view containing a String, to be retrieved later with View.getTag() or searched for with View.findViewWithTag() . [string] android:scrollX : The initial horizontal scroll offset, in pixels. [dimension] android:scrollY : The initial vertical scroll offset, in pixels. [dimension] -android:background : A drawable to use as the background. [reference, color] +android:background : A drawable to use as the background. [color, reference] android:padding : Sets the padding, in pixels, of all four edges. [dimension] android:paddingLeft : Sets the padding, in pixels, of the left edge; see padding. [dimension] android:paddingTop : Sets the padding, in pixels, of the top edge; see padding. [dimension] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completionvalues1-expected-completion32.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completionvalues1-expected-completion32.txt index 3592e04..7e27fa6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completionvalues1-expected-completion32.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/completionvalues1-expected-completion32.txt @@ -14,7 +14,7 @@ android:animationResolution : Timeout between frames of animation in millisecond android:autoLink : Controls whether links such as urls and email addresses are automatically found and converted to clickable links. [flag] android:autoStart : When true, automatically start animating [boolean] android:autoText : If set, specifies that this TextView has a textual input method and automatically corrects some common spelling errors. * Deprecated: Use inputType instead. [boolean] -android:background : A drawable to use as the background. [reference, color] +android:background : A drawable to use as the background. [color, reference] android:baseline : The offset of the baseline within this view. [dimension] android:baselineAlignBottom : If true, the image view will be baseline aligned with based on its bottom edge. [boolean] android:baselineAligned : When set to false, prevents the layout from aligning its children's baselines. [boolean] @@ -28,7 +28,7 @@ android:capitalize : If set, specifies that this TextView has a textual input me android:checkMark : Drawable used for the check mark graphic. [reference] android:checked : Indicates the initial checked state of this button. [boolean] android:checkedButton : The id of the child radio button that should be checked by default within this radio group. [integer] -android:childDivider : Drawable or color that is used as a divider for children. [reference, color] +android:childDivider : Drawable or color that is used as a divider for children. [color, reference] android:childIndicator : Indicator shown beside the child View. [reference] android:childIndicatorLeft : The left bound for a child's indicator. [dimension] android:childIndicatorRight : The right bound for a child's indicator. [dimension] @@ -55,16 +55,16 @@ android:divider : Drawable to use as a vertical divider between buttons. android:dividerHeight : Height of the divider. [dimension] android:dividerPadding : Size of padding on either end of a divider. [dimension] android:drawSelectorOnTop : When set to true, the selector will be drawn over the selected item. [boolean] -android:drawableBottom : The drawable to be drawn below the text. [reference, color] -android:drawableLeft : The drawable to be drawn to the left of the text. [reference, color] +android:drawableBottom : The drawable to be drawn below the text. [color, reference] +android:drawableLeft : The drawable to be drawn to the left of the text. [color, reference] android:drawablePadding : The padding between the drawables and the text. [dimension] -android:drawableRight : The drawable to be drawn to the right of the text. [reference, color] -android:drawableTop : The drawable to be drawn above the text. [reference, color] +android:drawableRight : The drawable to be drawn to the right of the text. [color, reference] +android:drawableTop : The drawable to be drawn above the text. [color, reference] android:drawingCacheQuality : Defines the quality of translucent drawing caches. [enum] android:dropDownAnchor : View to anchor the auto-complete dropdown to. [reference] android:dropDownHeight : Specifies the basic height of the dropdown. [dimension, enum] android:dropDownHorizontalOffset : Horizontal offset from the spinner widget for positioning the dropdown in spinnerMode="dropdown". [dimension] -android:dropDownSelector : List selector to use for spinnerMode="dropdown" display. [reference, color] +android:dropDownSelector : List selector to use for spinnerMode="dropdown" display. [color, reference] android:dropDownVerticalOffset : Vertical offset from the spinner widget for positioning the dropdown in spinnerMode="dropdown". [dimension] android:dropDownWidth : Width of the dropdown in spinnerMode="dropdown". [dimension, enum] android:duplicateParentState : When this attribute is set to true, the view gets its drawable state (focused, pressed, etc.) from its direct parent rather than from itself. [boolean] @@ -92,9 +92,9 @@ android:flingable : @hide Whether the number picker supports fligning. [boolean android:flipInterval : [integer] android:focusable : Boolean that controls whether a view can take focus. [boolean] android:focusableInTouchMode : Boolean that controls whether a view can take focus while in touch mode. [boolean] -android:focusedMonthDateColor : The color for the dates of the selected month. [reference, color] +android:focusedMonthDateColor : The color for the dates of the selected month. [color, reference] android:footerDividersEnabled : When set to false, the ListView will not draw the divider before each footer view. [boolean] -android:foreground : Defines the drawable to draw over the content. [reference, color] +android:foreground : Defines the drawable to draw over the content. [color, reference] android:foregroundGravity : Defines the gravity to apply to the foreground drawable. [flag] android:foregroundInsidePadding : Defines whether the foreground drawable should be drawn inside the padding. [boolean] android:format : Format string: if specified, the Chronometer will display this string, with the first "%s" replaced by the current timer value in "MM:SS" or "H:MM:SS" form. [string] @@ -144,7 +144,7 @@ android:lineSpacingExtra : Extra spacing between lines of text. [dimension] android:lineSpacingMultiplier : Extra spacing between lines of text, as a multiplier. [float] android:lines : Makes the TextView be exactly this many lines tall. [integer] android:linksClickable : If set to false, keeps the movement method from being set to the link movement method even if autoLink causes links to be found. [boolean] -android:listSelector : Drawable used to indicate the currently selected item in the list. [reference, color] +android:listSelector : Drawable used to indicate the currently selected item in the list. [color, reference] android:longClickable : Defines whether this view reacts to long click events. [boolean] android:loopViews : Defines whether the animator loops to the first view once it has reached the end of the list. [boolean] android:marqueeRepeatLimit : The number of times to repeat the marquee animation. [integer, enum] @@ -175,8 +175,8 @@ android:numeric : If set, specifies that this TextView has a numeric input metho android:onClick : Name of the method in this View's context to invoke when the view is clicked. [string] android:orientation : Should the layout be a column or a row? Use "horizontal" for a row, "vertical" for a column. [enum] android:outAnimation : Identifier for the animation to use when a view is hidden. [reference] -android:overScrollFooter : Drawable to draw below list content. [reference, color] -android:overScrollHeader : Drawable to draw above list content. [reference, color] +android:overScrollFooter : Drawable to draw below list content. [color, reference] +android:overScrollHeader : Drawable to draw above list content. [color, reference] android:overScrollMode : Defines over-scrolling behavior. [enum] android:padding : Sets the padding, in pixels, of all four edges. [dimension] android:paddingBottom : Sets the padding, in pixels, of the bottom edge; see padding. [dimension] @@ -186,7 +186,7 @@ android:paddingTop : Sets the padding, in pixels, of the top edge; see padding. android:password : Whether the characters of the field are displayed as password dots instead of themselves. * Deprecated: Use inputType instead. [boolean] android:persistentDrawingCache : Defines the persistence of the drawing cache. [flag] android:phoneNumber : If set, specifies that this TextView has a phone number input method. * Deprecated: Use inputType instead. [boolean] -android:popupBackground : Background drawable to use for the dropdown in spinnerMode="dropdown". [reference, color] +android:popupBackground : Background drawable to use for the dropdown in spinnerMode="dropdown". [color, reference] android:popupPromptView : Reference to a layout to use for displaying a prompt in the dropdown for spinnerMode="dropdown". [reference] android:privateImeOptions : An addition content type description to supply to the input method attached to the text view, which is private to the implementation of the input method. [string] android:progress : Defines the default progress value, between 0 and max. [integer] @@ -220,7 +220,7 @@ android:scrollingCache : When set to true, the list uses a drawing cache during android:secondaryProgress : Defines the secondary progress value, between 0 and max. [integer] android:selectAllOnFocus : If the text is selectable, select it all when the view takes focus instead of moving the cursor to the start or end. [boolean] android:selectedDateVerticalBar : Drawable for the vertical bar shown at the beggining and at the end of a selected date. [reference] -android:selectedWeekBackgroundColor : The background color for the selected week. [reference, color] +android:selectedWeekBackgroundColor : The background color for the selected week. [color, reference] android:selectionDivider : @hide The divider for making the selection area. [reference] android:selectionDividerHeight : @hide The height of the selection divider. [dimension] android:shadowColor : Place a shadow of the specified color behind the text. [color] @@ -233,13 +233,13 @@ android:shownWeekCount : The number of weeks to be shown. [integer] android:shrinkColumns : The zero-based index of the columns to shrink. [string] android:singleLine : Constrains the text to a single horizontally scrolling line instead of letting it wrap onto multiple lines, and advances focus instead of inserting a newline when you press the enter key. * Deprecated: This attribute is deprecated and is replaced by the textMultiLine flag in the inputType attribute. Use caution when altering existing layouts, as the default value of singeLine is false (multi-line mode), but if you specify any value for inputType, the default is single-line mode. (If both singleLine and inputType attributes are found, the inputType flags will override the value of singleLine.). [boolean] android:smoothScrollbar : When set to true, the list will use a more refined calculation method based on the pixels height of the items visible on screen. [boolean] -android:solidColor : @hide Color for the solid color background if such for optimized rendering. [reference, color] +android:solidColor : @hide Color for the solid color background if such for optimized rendering. [color, reference] android:soundEffectsEnabled : Boolean that controls whether a view should have sound effects enabled for events such as clicking and touching. [boolean] android:spacing : [dimension] android:spinnerMode : Display mode for spinner options. [enum] android:spinnersShown : Whether the spinners are shown. [boolean] android:splitMotionEvents : Sets whether this ViewGroup should split MotionEvents to separate child views during touch event dispatch. [boolean] -android:src : Sets a drawable as the content of this ImageView. [reference, color] +android:src : Sets a drawable as the content of this ImageView. [color, reference] android:stackFromBottom : Used by ListView and GridView to stack their content from the bottom. [boolean] android:startYear : The first year (inclusive), for example "1940". [integer] android:stepSize : The step size of the rating. [float] @@ -253,10 +253,10 @@ android:tabStripRight : Drawable used to draw the right part of the strip undern android:tag : Supply a tag for the top-level view containing a String, to be retrieved later with View.getTag() or searched for with View.findViewWithTag() . [string] android:text : Text to display. [string] android:textAppearance : Base text color, typeface, size, and style. [reference] -android:textColor : Text color. [reference, color] -android:textColorHighlight : Color of the text selection highlight. [reference, color] -android:textColorHint : Color of the hint text. [reference, color] -android:textColorLink : Text color for links. [reference, color] +android:textColor : Text color. [color, reference] +android:textColorHighlight : Color of the text selection highlight. [color, reference] +android:textColorHint : Color of the hint text. [color, reference] +android:textColorLink : Text color for links. [color, reference] android:textCursorDrawable : Reference to a drawable that will be drawn under the insertion cursor. [reference] android:textEditNoPasteWindowLayout : Variation of textEditPasteWindowLayout displayed when the clipboard is empty. [reference] android:textEditPasteWindowLayout : The layout of the view that is displayed on top of the cursor to paste inside a TextEdit field. [reference] @@ -283,13 +283,13 @@ android:translationX : translation in x of the view. [dimension] android:translationY : translation in y of the view. [dimension] android:typeface : Typeface (normal, sans, serif, monospace) for the text. [enum] android:uncertainGestureColor : Color used to draw the user's strokes until we are sure it's a gesture. [color] -android:unfocusedMonthDateColor : The color for the dates of an unfocused month. [reference, color] +android:unfocusedMonthDateColor : The color for the dates of an unfocused month. [color, reference] android:unselectedAlpha : Sets the alpha on the items that are not selected. [float] android:verticalScrollbarPosition : Determines which side the vertical scroll bar should be placed on. [enum] android:verticalSpacing : Defines the default vertical spacing between rows. [dimension] android:visibility : Controls the initial visibility of the view. [enum] android:weekDayTextAppearance : The text appearance for the week day abbreviation of the calendar header. [reference] -android:weekNumberColor : The color for the week numbers. [reference, color] -android:weekSeparatorLineColor : The color for the sepatator line between weeks. [reference, color] +android:weekNumberColor : The color for the week numbers. [color, reference] +android:weekSeparatorLineColor : The color for the sepatator line between weeks. [color, reference] android:weightSum : Defines the maximum weight sum. [float] android:width : Makes the TextView be exactly this many pixels wide. [dimension] diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion47.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion47.txt index edf4892..c7f495b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion47.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion47.txt @@ -1,5 +1,4 @@ Code completion in drawable1.xml for ^<layer-list: -<animated-rotate /> <animation-list /> : Drawable used to render several animated frames. <bitmap /> : Drawable used to draw bitmaps. <clip /> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion50.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion50.txt index 90eab10..d05bc76 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion50.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/drawable1-expected-completion50.txt @@ -1,5 +1,4 @@ Code completion in drawable1.xml for <item >^</item>: -<animated-rotate /> <animation-list /> : Drawable used to render several animated frames. <bitmap /> : Drawable used to draw bitmaps. <clip /> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/manifest-expected-completion69.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/manifest-expected-completion69.txt index 382011e..627ff01 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/manifest-expected-completion69.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/manifest-expected-completion69.txt @@ -1,19 +1,19 @@ Code completion in manifest.xml for <uses-sdk android:minSdkVersion="^11" />: -AOSP -14 : Android 4.0.1 -13 : Android 3.2 -12 : Android 3.1 -11 : Android 3.0 -10 -9 : Android 2.3 -8 : Android 2.2 -7 -6 -5 : Android 2.0 -4 : Android 1.6 -3 : Android 1.5 -2 : Android 1.1 -1 +15 : API 15: Android 4.0.3 (IceCreamSandwich) +14 : API 14: Android 4.0 (IceCreamSandwich) +13 : API 13: Android 3.2 (Honeycomb) +12 : API 12: Android 3.1 (Honeycomb) +11 : API 11: Android 3.0 (Honeycomb) +10 : API 10: Android 2.3.3 (Gingerbread) +9 : API 9: Android 2.3 (Gingerbread) +8 : API 8: Android 2.2 (Froyo) +7 : API 7: Android 2.1 (Eclair) +6 : API 6: Android 2.0.1 (Eclair) +5 : API 5: Android 2.0 (Eclair) +4 : API 4: Android 1.6 (Donut) +3 : API 3: Android 1.5 (Cupcake) +2 : API 2: Android 1.1 +1 : API 1: Android 1.0 @string/ @android: @+id/ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1-expected-navigate15.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1-expected-navigate15.txt new file mode 100644 index 0000000..e36c5f3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1-expected-navigate15.txt @@ -0,0 +1,7 @@ +Go To Declaration in navigation1.xml for ?android:attr/alert^DialogStyle: +Open Declaration in values/attrs.xml : [?android:attr/alertDialogStyle] + data/res/values/attrs.xml + + +After open, the selected text is: + <attr name="alertDialogStyle" format="reference" />^ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1.xml index 9c175fc..e7ac4bc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1.xml +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/navigation1.xml @@ -13,4 +13,5 @@ <EditText android:text="@android:string/ok" </EditText> + <EditText android:text="?android:attr/alertDialogStyle" /> </LinearLayout> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java index 9eeb2ea..12ac36f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java @@ -115,6 +115,24 @@ public class AdtUtilsTest extends TestCase { assertNull(null, AdtUtils.capitalize(null)); } + public void testCamelCaseToUnderlines() { + assertEquals("", AdtUtils.camelCaseToUnderlines("")); + assertEquals("foo", AdtUtils.camelCaseToUnderlines("foo")); + assertEquals("foo", AdtUtils.camelCaseToUnderlines("Foo")); + assertEquals("foo_bar", AdtUtils.camelCaseToUnderlines("FooBar")); + assertEquals("test_xml", AdtUtils.camelCaseToUnderlines("testXML")); + assertEquals("test_foo", AdtUtils.camelCaseToUnderlines("testFoo")); + } + + public void testUnderlinesToCamelCase() { + assertEquals("", AdtUtils.underlinesToCamelCase("")); + assertEquals("", AdtUtils.underlinesToCamelCase("_")); + assertEquals("Foo", AdtUtils.underlinesToCamelCase("foo")); + assertEquals("FooBar", AdtUtils.underlinesToCamelCase("foo_bar")); + assertEquals("FooBar", AdtUtils.underlinesToCamelCase("foo__bar")); + assertEquals("Foo", AdtUtils.underlinesToCamelCase("foo_")); + } + public void testStripSuffix() { assertEquals("Foo", AdtUtils.stripSuffix("Foo", "")); assertEquals("Fo", AdtUtils.stripSuffix("Foo", "o")); diff --git a/eclipse/scripts/create_all_symlinks.sh b/eclipse/scripts/create_all_symlinks.sh index 2ec32eb..dbc2cb8 100755 --- a/eclipse/scripts/create_all_symlinks.sh +++ b/eclipse/scripts/create_all_symlinks.sh @@ -127,6 +127,7 @@ ADT_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.adt/libs" ADT_LIBS="ant-glob assetstudio ide_common layoutlib_api lint_api lint_checks ninepatch propertysheet rule_api sdkuilib swtmenubar manifmerger" ADT_PREBUILTS="\ prebuilts/misc/common/kxml2/kxml2-2.3.0.jar \ + prebuilts/tools/common/freemarker/freemarker-2.3.19.jar \ prebuilts/tools/common/asm-tools/asm-4.0.jar \ prebuilts/tools/common/asm-tools/asm-tree-4.0.jar \ prebuilts/tools/common/lombok-ast/lombok-ast-0.2.jar" |