aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java326
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java107
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmCamelCaseToUnderscoreMethod.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java61
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmSlashedPackageNameMethod.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmUnderscoreToCamelCaseMethod.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java298
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java187
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java931
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java456
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java125
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java946
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java212
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java417
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java380
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/StringEvaluator.java101
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java1239
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java261
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java468
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java161
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java78
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java222
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java94
32 files changed, 7796 insertions, 0 deletions
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 000000000..ba4aedc8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java
@@ -0,0 +1,326 @@
+/*
+ * 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.CATEGORY_ACTIVITIES;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_OTHER;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LAUNCHER;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import org.eclipse.core.runtime.IStatus;
+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.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+class ActivityPage extends WizardPage implements SelectionListener {
+ private final NewProjectWizardState mValues;
+ private List mList;
+ private Button mCreateToggle;
+ private java.util.List<File> mTemplates;
+
+ private boolean mIgnore;
+ private boolean mShown;
+ private ImageControl mPreview;
+ private Image mPreviewImage;
+ private boolean mDisposePreviewImage;
+ private Label mHeading;
+ private Label mDescription;
+ private boolean mOnlyActivities;
+ private boolean mAskCreate;
+ private boolean mLauncherActivitiesOnly;
+
+ /**
+ * Create the wizard.
+ */
+ ActivityPage(NewProjectWizardState values, boolean onlyActivities, boolean askCreate) {
+ super("activityPage"); //$NON-NLS-1$
+ mValues = values;
+ mOnlyActivities = onlyActivities;
+ mAskCreate = askCreate;
+
+ if (onlyActivities) {
+ setTitle("Create Activity");
+ } else {
+ setTitle("Create Android Object");
+ }
+ if (onlyActivities && askCreate) {
+ setDescription(
+ "Select whether to create an activity, and if so, what kind of activity.");
+ } else {
+ setDescription("Select which template to use");
+ }
+ }
+
+ /** Sets whether the activity page should only offer launcher activities */
+ void setLauncherActivitiesOnly(boolean launcherActivitiesOnly) {
+ mLauncherActivitiesOnly = launcherActivitiesOnly;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects and aren't unused
+ private void onEnter() {
+ Composite container = (Composite) getControl();
+ container.setLayout(new GridLayout(3, false));
+
+ if (mAskCreate) {
+ 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));
+
+
+ TemplateManager manager = mValues.template.getManager();
+ java.util.List<File> templates = manager.getTemplates(CATEGORY_ACTIVITIES);
+
+ if (!mOnlyActivities) {
+ templates.addAll(manager.getTemplates(CATEGORY_OTHER));
+ }
+ java.util.List<String> names = new ArrayList<String>(templates.size());
+ File current = mValues.activityValues.getTemplateLocation();
+ mTemplates = Lists.newArrayListWithExpectedSize(templates.size());
+ int index = -1;
+ for (int i = 0, n = templates.size(); i < n; i++) {
+ File template = templates.get(i);
+ TemplateMetadata metadata = manager.getTemplate(template);
+ if (metadata == null) {
+ continue;
+ }
+ if (mLauncherActivitiesOnly) {
+ Parameter parameter = metadata.getParameter(IS_LAUNCHER);
+ if (parameter == null) {
+ continue;
+ }
+ }
+ mTemplates.add(template);
+ names.add(metadata.getTitle());
+ if (template.equals(current)) {
+ index = names.size();
+ }
+ }
+ String[] items = names.toArray(new String[names.size()]);
+ mList.setItems(items);
+ if (index == -1 && !mTemplates.isEmpty()) {
+ mValues.activityValues.setTemplateLocation(mTemplates.get(0));
+ index = 0;
+ }
+ if (index >= 0) {
+ mList.setSelection(index);
+ mList.addSelectionListener(this);
+ }
+
+ // Preview
+ mPreview = new ImageControl(container, SWT.NONE, null);
+ mPreview.setDisposeImage(false); // Handled manually in this class
+ 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);
+ }
+
+ updatePreview();
+ }
+
+ private void updatePreview() {
+ Image oldImage = mPreviewImage;
+ boolean dispose = mDisposePreviewImage;
+ mPreviewImage = null;
+
+ String title = "";
+ String description = "";
+ TemplateHandler handler = mValues.activityValues.getTemplateHandler();
+ TemplateMetadata template = handler.getTemplate();
+ if (template != null) {
+ String thumb = template.getThumbnailPath();
+ if (thumb != null && !thumb.isEmpty()) {
+ File file = new File(mValues.activityValues.getTemplateLocation(),
+ thumb.replace('/', File.separatorChar));
+ if (file != null) {
+ try {
+ byte[] bytes = Files.toByteArray(file);
+ ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+ mPreviewImage = new Image(getControl().getDisplay(), input);
+ mDisposePreviewImage = true;
+ input.close();
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ } else {
+ // Fallback icon
+ mDisposePreviewImage = false;
+ mPreviewImage = TemplateMetadata.getDefaultTemplateIcon();
+ }
+ title = template.getTitle();
+ description = template.getDescription();
+ }
+
+ mHeading.setText(title);
+ mDescription.setText(description);
+ mPreview.setImage(mPreviewImage);
+ mPreview.fitToWidth(PREVIEW_WIDTH);
+
+ if (oldImage != null && dispose) {
+ oldImage.dispose();
+ }
+
+ Composite parent = (Composite) getControl();
+ parent.layout(true, true);
+ parent.redraw();
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mPreviewImage != null && mDisposePreviewImage) {
+ mDisposePreviewImage = false;
+ mPreviewImage.dispose();
+ mPreviewImage = null;
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible && !mShown) {
+ onEnter();
+ }
+
+ super.setVisible(visible);
+
+ if (visible) {
+ mShown = true;
+ if (mAskCreate) {
+ try {
+ mIgnore = true;
+ mCreateToggle.setSelection(mValues.createActivity);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+
+ private void validatePage() {
+ IStatus status = null;
+
+ if (mValues.createActivity) {
+ if (mList.getSelectionCount() < 1) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Select an activity type");
+ } else {
+ TemplateHandler templateHandler = mValues.activityValues.getTemplateHandler();
+ status = templateHandler.validateTemplate(mValues.minSdkLevel,
+ mValues.getBuildApi());
+ }
+ }
+
+ 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() {
+ // Ensure that the Finish button isn't enabled until
+ // the user has reached and completed this page
+ if (!mShown && mValues.createActivity) {
+ 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();
+ if (index >= 0 && index < mTemplates.size()) {
+ File template = mTemplates.get(index);
+ mValues.activityValues.setTemplateLocation(template);
+ updatePreview();
+ }
+ }
+
+ 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/CreateFileChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java
new file mode 100644
index 000000000..3b41c36c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/CreateFileChange.java
@@ -0,0 +1,107 @@
+/*
+ * 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.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.google.common.io.Closeables;
+import com.google.common.io.Files;
+import com.google.common.io.InputSupplier;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.resource.ResourceChange;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+
+/** Change which lazily copies a file */
+public class CreateFileChange extends ResourceChange {
+ private String mName;
+ private final IPath mPath;
+ private final File mSource;
+
+ CreateFileChange(@NonNull String name, @NonNull IPath workspacePath, File source) {
+ mName = name;
+ mPath = workspacePath;
+ mSource = source;
+ }
+
+ @Override
+ protected IResource getModifiedResource() {
+ return ResourcesPlugin.getWorkspace().getRoot().getFile(mPath);
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public RefactoringStatus isValid(IProgressMonitor pm)
+ throws CoreException, OperationCanceledException {
+ RefactoringStatus result = new RefactoringStatus();
+ IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(mPath);
+ URI location = file.getLocationURI();
+ if (location == null) {
+ result.addFatalError("Unknown location " + file.getFullPath().toString());
+ return result;
+ }
+ return result;
+ }
+
+ @SuppressWarnings("resource") // Eclipse doesn't know about Guava's Closeables.closeQuietly
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ InputSupplier<FileInputStream> supplier = Files.newInputStreamSupplier(mSource);
+ InputStream is = null;
+ try {
+ pm.beginTask("Creating file", 3);
+ IFile file = (IFile) getModifiedResource();
+
+ IContainer parent = file.getParent();
+ if (parent != null && !parent.exists()) {
+ IFolder folder = ResourcesPlugin.getWorkspace().getRoot().getFolder(
+ parent.getFullPath());
+ AdtUtils.ensureExists(folder);
+ }
+
+ is = supplier.getInput();
+ file.create(is, false, new SubProgressMonitor(pm, 1));
+ pm.worked(1);
+ } catch (Exception ioe) {
+ AdtPlugin.log(ioe, null);
+ } finally {
+ Closeables.closeQuietly(is);
+ pm.done();
+ }
+ return null;
+ }
+}
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 000000000..fbd50e986
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmActivityToLayoutMethod.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.ACTIVITY_NAME_SUFFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.LAYOUT_NAME_PREFIX;
+
+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();
+
+ if (activityName.isEmpty()) {
+ return new SimpleScalar("");
+ }
+
+ // 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 = LAYOUT_NAME_PREFIX + 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 000000000..b85576577
--- /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.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/FmClassNameToResourceMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java
new file mode 100644
index 000000000..366de9afa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmClassNameToResourceMethod.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+/**
+ * Similar to {@link FmCamelCaseToUnderscoreMethod}, but strips off common class
+ * suffixes such as "Activity", "Fragment", etc.
+ */
+public class FmClassNameToResourceMethod 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();
+
+ if (name.isEmpty()) {
+ return new SimpleScalar("");
+ }
+
+ name = stripSuffix(name, ACTIVITY_NAME_SUFFIX);
+ name = stripSuffix(name, "Fragment"); //$NON-NLS-1$
+ name = stripSuffix(name, "Service"); //$NON-NLS-1$
+ name = stripSuffix(name, "Provider"); //$NON-NLS-1$
+
+ return new SimpleScalar(AdtUtils.camelCaseToUnderlines(name));
+ }
+
+ // 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
+ private static String stripSuffix(String name, String suffix) {
+ int suffixStart = name.lastIndexOf(suffix.charAt(0));
+ if (suffixStart != -1 && name.regionMatches(suffixStart, suffix, 0,
+ name.length() - suffixStart)) {
+ name = name.substring(0, suffixStart);
+ }
+ assert !name.endsWith(suffix) : name;
+
+ return 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/FmEscapeXmlAttributeMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java
new file mode 100644
index 000000000..21f33b8d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlAttributeMethod.java
@@ -0,0 +1,40 @@
+/*
+ * 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.utils.XmlUtils;
+
+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 escape a string such that it can be used
+ * as an XML attribute (escaping ', ", & and <).
+ */
+public class FmEscapeXmlAttributeMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(XmlUtils.toXmlAttributeValue(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
new file mode 100644
index 000000000..2255653a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import com.android.ide.common.res2.ValueXmlHelper;
+
+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 escape a string such that it can be placed
+ * as text in a string resource file.
+ * This is similar to {@link FmEscapeXmlTextMethod}, but in addition to escaping
+ * &lt; and &amp; it also escapes characters such as quotes necessary for Android
+ *{@code <string>} elements.
+ */
+public class FmEscapeXmlStringMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(ValueXmlHelper.escapeResourceString(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java
new file mode 100644
index 000000000..55a4bc8ab
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlTextMethod.java
@@ -0,0 +1,40 @@
+/*
+ * 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.utils.XmlUtils;
+
+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 escape a string such that it can be used
+ * as XML text (escaping < and &, but not ' and " etc).
+ */
+public class FmEscapeXmlTextMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ return new SimpleScalar(XmlUtils.toXmlTextValue(string));
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java
new file mode 100644
index 000000000..09fa81c57
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmExtractLettersMethod.java
@@ -0,0 +1,45 @@
+/*
+ * 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 extract letters from a string; this will remove
+ * any whitespace, punctuation and digits.
+ */
+public class FmExtractLettersMethod implements TemplateMethodModel {
+ @Override
+ public TemplateModel exec(List args) throws TemplateModelException {
+ if (args.size() != 1) {
+ throw new TemplateModelException("Wrong arguments");
+ }
+ String string = args.get(0).toString();
+ StringBuilder sb = new StringBuilder(string.length());
+ for (int i = 0, n = string.length(); i < n; i++) {
+ char c = string.charAt(i);
+ if (Character.isLetter(c)) {
+ sb.append(c);
+ }
+ }
+ return new SimpleScalar(sb.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 000000000..6514959f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmLayoutToActivityMethod.java
@@ -0,0 +1,61 @@
+/*
+ * 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 static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectPage.LAYOUT_NAME_PREFIX;
+
+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();
+
+ // Strip off the beginning portion of the layout 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").
+ if (name.startsWith(LAYOUT_NAME_PREFIX)) {
+ name = name.substring(LAYOUT_NAME_PREFIX.length());
+ }
+
+ 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 000000000..60a6531e6
--- /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 000000000..26d4fadb4
--- /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.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/InstallDependencyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java
new file mode 100644
index 000000000..d806e7970
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/InstallDependencyPage.java
@@ -0,0 +1,298 @@
+/*
+ * 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 com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.wizard.IWizard;
+import org.eclipse.jface.wizard.IWizardPage;
+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;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.browser.IWebBrowser;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+
+class InstallDependencyPage extends WizardPage implements SelectionListener {
+ /**
+ * The compatibility library. This is the only library the templates
+ * currently support. The appearance of any other dependency in this
+ * template will be flagged as a validation error (and the user encouraged
+ * to upgrade to a newer ADT
+ */
+ static final String SUPPORT_LIBRARY_NAME = "android-support-v4"; //$NON-NLS-1$
+
+ /** URL containing more info */
+ private static final String URL =
+ "http://developer.android.com/tools/extras/support-library.html"; //$NON-NLS-1$
+
+ private Button mCheckButton;
+ private Button mInstallButton;
+ private Link mLink;
+ private TemplateMetadata mTemplate;
+
+ InstallDependencyPage() {
+ super("dependency"); //$NON-NLS-1$
+ setTitle("Install Dependencies");
+ }
+
+ void setTemplate(TemplateMetadata template) {
+ if (template != mTemplate) {
+ mTemplate = template;
+ if (getControl() != null) {
+ validatePage();
+ }
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ updateVersionLabels();
+ validatePage();
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(2, false));
+ // Remaining contents are created lazily, since this page is always added to
+ // the page list, but typically not shown
+
+ Label dependLabel = new Label(container, SWT.WRAP);
+ GridData gd_dependLabel = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
+ gd_dependLabel.widthHint = NewTemplatePage.WIZARD_PAGE_WIDTH - 50;
+ dependLabel.setLayoutData(gd_dependLabel);
+ dependLabel.setText("This template depends on the Android Support library, which is " +
+ "either not installed, or the template depends on a more recent version than " +
+ "the one you have installed.");
+
+ mLink = new Link(container, SWT.NONE);
+ mLink.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
+ mLink.setText("<a href=\"" + URL + "\">" + URL + "</a>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ mLink.addSelectionListener(this);
+
+ Label lblNewLabel_1 = new Label(container, SWT.NONE);
+ lblNewLabel_1.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+
+ requiredLabel = new Label(container, SWT.NONE);
+ requiredLabel.setText("Required version:");
+
+ mRequiredVersion = new Label(container, SWT.NONE);
+ mRequiredVersion.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+
+ installedLabel = new Label(container, SWT.NONE);
+ installedLabel.setText("Installed version:");
+
+ mInstalledVersion = new Label(container, SWT.NONE);
+ mInstalledVersion.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+
+ Label lblNewLabel = new Label(container, SWT.NONE);
+ lblNewLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+
+ Label descLabel = new Label(container, SWT.WRAP);
+ GridData gd_descLabel = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
+ gd_descLabel.widthHint = 550;
+ descLabel.setLayoutData(gd_descLabel);
+ descLabel.setText(
+ "You can install or upgrade it by clicking the Install button below, or " +
+ "alternatively, you can install it outside of Eclipse with the SDK Manager, " +
+ "then click on \"Check Again\" to proceed.");
+
+ mInstallButton = new Button(container, SWT.NONE);
+ mInstallButton.setText("Install/Upgrade");
+ mInstallButton.addSelectionListener(this);
+
+ mCheckButton = new Button(container, SWT.NONE);
+ mCheckButton.setText("Check Again");
+ mCheckButton.addSelectionListener(this);
+
+ mInstallButton.setFocus();
+ }
+
+ private void showNextPage() {
+ validatePage();
+ if (isPageComplete()) {
+ // Finish button will be enabled now
+ mInstallButton.setEnabled(false);
+ mCheckButton.setEnabled(false);
+
+ IWizard wizard = getWizard();
+ IWizardPage next = wizard.getNextPage(this);
+ if (next != null) {
+ wizard.getContainer().showPage(next);
+ }
+ }
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ if (mTemplate == null) {
+ return true;
+ }
+
+ return super.isPageComplete() && isInstalled();
+ }
+
+ private boolean isInstalled() {
+ return isInstalled(mTemplate.getDependencies());
+ }
+
+ static String sCachedName;
+ static int sCachedVersion;
+ private Label requiredLabel;
+ private Label installedLabel;
+ private Label mRequiredVersion;
+ private Label mInstalledVersion;
+
+ public static boolean isInstalled(List<Pair<String, Integer>> dependencies) {
+ for (Pair<String, Integer> dependency : dependencies) {
+ String name = dependency.getFirst();
+ int required = dependency.getSecond();
+
+ int installed = -1;
+ if (SUPPORT_LIBRARY_NAME.equals(name)) {
+ installed = getInstalledSupportLibVersion();
+ }
+
+ if (installed == -1) {
+ return false;
+ }
+ if (required > installed) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static int getInstalledSupportLibVersion() {
+ if (SUPPORT_LIBRARY_NAME.equals(sCachedName)) {
+ return sCachedVersion;
+ } else {
+ int version = AddSupportJarAction.getInstalledRevision();
+ sCachedName = SUPPORT_LIBRARY_NAME;
+ sCachedVersion = version;
+ return version;
+ }
+ }
+
+ private void updateVersionLabels() {
+ int version = getInstalledSupportLibVersion();
+ if (version == -1) {
+ mInstalledVersion.setText("Not installed");
+ } else {
+ mInstalledVersion.setText(Integer.toString(version));
+ }
+
+ if (mTemplate != null) {
+ for (Pair<String, Integer> dependency : mTemplate.getDependencies()) {
+ String name = dependency.getFirst();
+ if (name.equals(SUPPORT_LIBRARY_NAME)) {
+ int required = dependency.getSecond();
+ mRequiredVersion.setText(Integer.toString(required));
+ break;
+ }
+ }
+ }
+ }
+
+ private void validatePage() {
+ if (mTemplate == null) {
+ return;
+ }
+
+ IStatus status = null;
+
+ List<Pair<String, Integer>> dependencies = mTemplate.getDependencies();
+ if (dependencies.size() > 1 || dependencies.size() == 1
+ && !dependencies.get(0).getFirst().equals(SUPPORT_LIBRARY_NAME)) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "Unsupported template dependency: Upgrade your Android Eclipse plugin");
+ }
+
+ 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) {
+ Object source = e.getSource();
+ if (source == mCheckButton) {
+ sCachedName = null;
+ if (isInstalled()) {
+ showNextPage();
+ }
+ updateVersionLabels();
+ } else if (source == mInstallButton) {
+ sCachedName = null;
+ for (Pair<String, Integer> dependency : mTemplate.getDependencies()) {
+ String name = dependency.getFirst();
+ if (SUPPORT_LIBRARY_NAME.equals(name)) {
+ int version = dependency.getSecond();
+ File installed = AddSupportJarAction.installSupport(version);
+ if (installed != null) {
+ showNextPage();
+ }
+ updateVersionLabels();
+ }
+ }
+ } else if (source == mLink) {
+ try {
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
+ browser.openURL(new URL(URL));
+ } catch (Exception ex) {
+ String message = String.format("Could not open browser. Vist\n%1$s\ninstead.",
+ URL);
+ MessageDialog.openError(getShell(), "Browser Error", message);
+ }
+ }
+ }
+
+ @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/NewActivityWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java
new file mode 100644
index 000000000..b33d65bb7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewActivityWizard.java
@@ -0,0 +1,187 @@
+/*
+ * 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_BUILD_API;
+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 org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ui.IWorkbench;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Wizard for creating new activities. This is a hybrid between a New Project
+ * Wizard and a New Template Wizard: it has the "Activity selector" page from
+ * the New Project Wizard, which is used to dynamically select a wizard for the
+ * second page, but beyond that it runs the normal template wizard when it comes
+ * time to create the template.
+ */
+public class NewActivityWizard extends TemplateWizard {
+ private NewTemplatePage mTemplatePage;
+ private ActivityPage mActivityPage;
+ private NewProjectWizardState mValues;
+ private NewTemplateWizardState mActivityValues;
+ protected boolean mOnlyActivities;
+
+ /** Creates a new {@link NewActivityWizard} */
+ public NewActivityWizard() {
+ mOnlyActivities = true;
+ }
+
+ @Override
+ protected boolean shouldAddIconPage() {
+ return mActivityValues.getIconState() != null;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ setWindowTitle(mOnlyActivities ? "New Activity" : "New Android Object");
+
+ mValues = new NewProjectWizardState();
+ mActivityPage = new ActivityPage(mValues, mOnlyActivities, false);
+
+ mActivityValues = mValues.activityValues;
+ List<IProject> projects = AdtUtils.getSelectedProjects(selection);
+ if (projects.size() == 1) {
+ mActivityValues.project = projects.get(0);
+ }
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mActivityPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mActivityPage) {
+ if (mTemplatePage == null) {
+ Set<String> hidden = mActivityValues.hidden;
+ hidden.add(ATTR_PACKAGE_NAME);
+ hidden.add(ATTR_MIN_API);
+ hidden.add(ATTR_MIN_API_LEVEL);
+ hidden.add(ATTR_TARGET_API);
+ hidden.add(ATTR_BUILD_API);
+
+ mTemplatePage = new NewTemplatePage(mActivityValues, true);
+ addPage(mTemplatePage);
+ }
+ return mTemplatePage;
+ } else if (page == mTemplatePage && shouldAddIconPage()) {
+ WizardPage iconPage = getIconPage(mActivityValues.getIconState());
+ mActivityValues.updateIconState(mTemplatePage.getEvaluator());
+ return iconPage;
+ } else if (page == mTemplatePage
+ || shouldAddIconPage() && page == getIconPage(mActivityValues.getIconState())) {
+ TemplateMetadata template = mActivityValues.getTemplateHandler().getTemplate();
+ if (template != null) {
+ if (InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getPreviewPage(mActivityValues);
+ } else {
+ return getDependencyPage(template, true);
+ }
+ }
+ } else {
+ TemplateMetadata template = mActivityValues.getTemplateHandler().getTemplate();
+ if (template != null && page == getDependencyPage(template, false)) {
+ return getPreviewPage(mActivityValues);
+ }
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ public boolean canFinish() {
+ // 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 (mTemplatePage == null || !mTemplatePage.isPageComplete()) {
+ return false;
+ }
+
+ return super.canFinish();
+ }
+
+ @Override
+ public boolean performFinish(IProgressMonitor monitor) throws InvocationTargetException {
+ boolean success = super.performFinish(monitor);
+
+ if (success) {
+ List<Runnable> finalizingTasks = getFinalizingActions();
+ for (Runnable r : finalizingTasks) {
+ r.run();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @NonNull
+ protected IProject getProject() {
+ return mActivityValues.project;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ TemplateHandler activityTemplate = mActivityValues.getTemplateHandler();
+ return activityTemplate.getFilesToOpen();
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ TemplateHandler activityTemplate = mActivityValues.getTemplateHandler();
+ return activityTemplate.getFinalizingActions();
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ return mActivityValues.computeChanges();
+ }
+
+ /** Wizard for creating other Android components */
+ public static class OtherWizard extends NewActivityWizard {
+ /** Create new {@link OtherWizard} */
+ public OtherWizard() {
+ mOnlyActivities = false;
+ }
+ }
+}
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 000000000..14f59c00d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java
@@ -0,0 +1,931 @@
+/*
+ * 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.SdkConstants.ATTR_ID;
+import static com.android.ide.eclipse.adt.AdtUtils.extractClassName;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplatePage.WIZARD_PAGE_WIDTH;
+
+import com.android.annotations.Nullable;
+import com.android.sdklib.SdkVersionInfo;
+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.Lists;
+import com.google.common.collect.Maps;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+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.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;
+
+/**
+ * First wizard page in the "New Project From Template" wizard
+ */
+public class NewProjectPage extends WizardPage
+ implements ModifyListener, SelectionListener, FocusListener {
+ private static final int FIELD_WIDTH = 300;
+ 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$
+ /** Prefix added to default layout names */
+ static final String LAYOUT_NAME_PREFIX = "activity_"; //$NON-NLS-1$
+ private static final int INITIAL_MIN_SDK = 8;
+
+ private final NewProjectWizardState mValues;
+ private Map<String, Integer> mMinNameToApi;
+ private Parameter mThemeParameter;
+ private Combo mThemeCombo;
+
+ private Text mProjectText;
+ private Text mPackageText;
+ private Text mApplicationText;
+ private Combo mMinSdkCombo;
+ private Combo mTargetSdkCombo;
+ private Combo mBuildSdkCombo;
+ private Label mHelpIcon;
+ private Label mTipLabel;
+
+ private boolean mIgnore;
+ private ControlDecoration mApplicationDec;
+ private ControlDecoration mProjectDec;
+ private ControlDecoration mPackageDec;
+ private ControlDecoration mBuildTargetDec;
+ private ControlDecoration mMinSdkDec;
+ private ControlDecoration mTargetSdkDec;
+ private ControlDecoration mThemeDec;
+
+ 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);
+ GridData gdApplicationText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdApplicationText.widthHint = FIELD_WIDTH;
+ mApplicationText.setLayoutData(gdApplicationText);
+ 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);
+ GridData gdProjectText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdProjectText.widthHint = FIELD_WIDTH;
+ mProjectText.setLayoutData(gdProjectText);
+ 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);
+ GridData gdPackageText = new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1);
+ gdPackageText.widthHint = FIELD_WIDTH;
+ mPackageText.setLayoutData(gdPackageText);
+ mPackageText.addModifyListener(this);
+ mPackageText.addFocusListener(this);
+ mPackageDec = createFieldDecoration(mPackageText,
+ "The package name must be a unique identifier for your application.\n" +
+ "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\".\nThis 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);
+
+ // Min SDK
+
+ 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);
+ GridData gdMinSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdMinSdkCombo.widthHint = FIELD_WIDTH;
+ mMinSdkCombo.setLayoutData(gdMinSdkCombo);
+
+ // Pick most recent platform
+ IAndroidTarget[] targets = getCompilationTargets();
+ mMinNameToApi = Maps.newHashMap();
+ List<String> targetLabels = new ArrayList<String>(targets.length);
+ for (IAndroidTarget target : targets) {
+ String targetLabel;
+ if (target.isPlatform()
+ && target.getVersion().getApiLevel() <= AdtUtils.getHighestKnownApiLevel()) {
+ targetLabel = AdtUtils.getAndroidName(target.getVersion().getApiLevel());
+ } else {
+ targetLabel = AdtUtils.getTargetLabel(target);
+ }
+ targetLabels.add(targetLabel);
+ mMinNameToApi.put(targetLabel, target.getVersion().getApiLevel());
+ }
+
+ List<String> codeNames = Lists.newArrayList();
+ int buildTargetIndex = -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 = codeName + " Preview";
+ codeNames.add(targetLabel);
+ mMinNameToApi.put(targetLabel, apiLevel);
+ } else if (target.isPlatform()
+ && (mValues.target == null ||
+ apiLevel > mValues.target.getVersion().getApiLevel())) {
+ mValues.target = target;
+ buildTargetIndex = i;
+ }
+ }
+ List<String> 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();
+ mValues.minSdkLevel = mValues.iconState.minSdk;
+ } else {
+ mMinSdkCombo.select(INITIAL_MIN_SDK - 1);
+ mValues.minSdk = Integer.toString(INITIAL_MIN_SDK);
+ mValues.minSdkLevel = 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 95% of the market.");
+ new Label(container, SWT.NONE);
+
+ // Target SDK
+ Label targetSdkLabel = new Label(container, SWT.NONE);
+ targetSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ targetSdkLabel.setText("Target SDK:");
+
+ mTargetSdkCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gdTargetSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdTargetSdkCombo.widthHint = FIELD_WIDTH;
+ mTargetSdkCombo.setLayoutData(gdTargetSdkCombo);
+
+ mTargetSdkCombo.setItems(versions);
+ mTargetSdkCombo.select(mValues.targetSdkLevel - 1);
+
+ mTargetSdkCombo.addSelectionListener(this);
+ mTargetSdkCombo.addFocusListener(this);
+ mTargetSdkDec = createFieldDecoration(mTargetSdkCombo,
+ "Choose the highest API level that the application is known to work with. " +
+ "This attribute informs the system that you have tested against the target " +
+ "version and the system should not enable any compatibility behaviors to " +
+ "maintain your app's forward-compatibility with the target version. " +
+ "The application is still able to run on older versions " +
+ "(down to minSdkVersion). Your application may look dated if you are not " +
+ "targeting the current version.");
+ new Label(container, SWT.NONE);
+
+ // Build Version
+
+ Label buildSdkLabel = new Label(container, SWT.NONE);
+ buildSdkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ buildSdkLabel.setText("Compile With:");
+
+ mBuildSdkCombo = new Combo(container, SWT.READ_ONLY);
+ GridData gdBuildSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdBuildSdkCombo.widthHint = FIELD_WIDTH;
+ mBuildSdkCombo.setLayoutData(gdBuildSdkCombo);
+ mBuildSdkCombo.setData(targets);
+ mBuildSdkCombo.setItems(targetLabels.toArray(new String[targetLabels.size()]));
+ if (buildTargetIndex != -1) {
+ mBuildSdkCombo.select(buildTargetIndex);
+ }
+
+ mBuildSdkCombo.addSelectionListener(this);
+ mBuildSdkCombo.addFocusListener(this);
+ mBuildTargetDec = createFieldDecoration(mBuildSdkCombo,
+ "Choose a target API to compile your code against, from your installed SDKs. " +
+ "This is typically the most recent version, or the first version that supports " +
+ "all the APIs you want to directly access without reflection.");
+ new Label(container, SWT.NONE);
+
+ TemplateMetadata metadata = mValues.template.getTemplate();
+ if (metadata != null) {
+ mThemeParameter = metadata.getParameter("baseTheme"); //$NON-NLS-1$
+ if (mThemeParameter != null && mThemeParameter.element != null) {
+ Label themeLabel = new Label(container, SWT.NONE);
+ themeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 2, 1));
+ themeLabel.setText("Theme:");
+
+ mThemeCombo = NewTemplatePage.createOptionCombo(mThemeParameter, container,
+ mValues.parameters, this, this);
+ GridData gdThemeCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1);
+ gdThemeCombo.widthHint = FIELD_WIDTH;
+ mThemeCombo.setLayoutData(gdThemeCombo);
+ new Label(container, SWT.NONE);
+
+ mThemeDec = createFieldDecoration(mThemeCombo,
+ "Choose the base theme to use for the application");
+ }
+ }
+
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+
+ 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);
+ }
+
+ /**
+ * Updates the theme selection such that it's valid for the current build
+ * and min sdk targets. Also runs {@link #validatePage} in case no valid entry was found.
+ * Does nothing if called on a template that does not supply a theme.
+ */
+ void updateTheme() {
+ if (mThemeParameter != null) {
+ // Pick the highest theme version that works for the current SDK level
+ Parameter parameter = NewTemplatePage.getParameter(mThemeCombo);
+ assert parameter == mThemeParameter;
+ if (parameter != null) {
+ String[] optionIds = (String[]) mThemeCombo.getData(ATTR_ID);
+ for (int index = optionIds.length - 1; index >= 0; index--) {
+ IStatus status = NewTemplatePage.validateCombo(null, mThemeParameter,
+ index, mValues.minSdkLevel, mValues.getBuildApi());
+ if (status == null || status.isOK()) {
+ String optionId = optionIds[index];
+ parameter.value = optionId;
+ parameter.edited = optionId != null && !optionId.toString().isEmpty();
+ mValues.parameters.put(parameter.id, optionId);
+ try {
+ mIgnore = true;
+ mThemeCombo.select(index);
+ } finally {
+ mIgnore = false;
+ }
+ break;
+ }
+ }
+ }
+
+ validatePage();
+ }
+ }
+
+ private IAndroidTarget[] getCompilationTargets() {
+ Sdk current = Sdk.getCurrent();
+ if (current == null) {
+ return new IAndroidTarget[0];
+ }
+ IAndroidTarget[] targets = current.getTargets();
+ List<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
+
+ for (IAndroidTarget target : targets) {
+ if (target.isPlatform() == false &&
+ (target.getOptionalLibraries() == null ||
+ target.getOptionalLibraries().length == 0)) {
+ continue;
+ }
+ list.add(target);
+ }
+
+ return list.toArray(new IAndroidTarget[list.size()]);
+ }
+
+ 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();
+ updateProjectLocation(mValues.projectName);
+ 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 = appNameToProjectName(mValues.applicationName);
+ mProjectText.setText(mValues.projectName);
+ updateProjectLocation(mValues.projectName);
+ }
+ updateActivityNames(mValues.applicationName);
+ } finally {
+ mIgnore = false;
+ }
+ suggestPackage(mValues.applicationName);
+ }
+
+ validatePage();
+ }
+
+ private String appNameToProjectName(String appName) {
+ // Strip out whitespace (and capitalize subsequent words where spaces were removed
+ boolean upcaseNext = false;
+ StringBuilder sb = new StringBuilder(appName.length());
+ for (int i = 0, n = appName.length(); i < n; i++) {
+ char c = appName.charAt(i);
+ if (c == ' ') {
+ upcaseNext = true;
+ } else if (upcaseNext) {
+ sb.append(Character.toUpperCase(c));
+ upcaseNext = false;
+ } else {
+ sb.append(c);
+ }
+ }
+
+ appName = sb.toString().trim();
+
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IStatus nameStatus = workspace.validateName(appName, IResource.PROJECT);
+ if (nameStatus.isOK()) {
+ return appName;
+ }
+
+ sb = new StringBuilder(appName.length());
+ for (int i = 0, n = appName.length(); i < n; i++) {
+ char c = appName.charAt(i);
+ if (Character.isLetterOrDigit(c) || c == '.' || c == '-') {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString().trim();
+ }
+
+ /** If the project should be created in the workspace, then update the project location
+ * based on the project name. */
+ private void updateProjectLocation(String projectName) {
+ if (projectName == null) {
+ projectName = "";
+ }
+
+ if (mValues.useDefaultLocation) {
+ IPath workspace = Platform.getLocation();
+ String projectLocation = workspace.append(projectName).toOSString();
+ mValues.projectLocation = projectLocation;
+ }
+ }
+
+ 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 == mMinSdkCombo) {
+ mValues.minSdk = getSelectedMinSdk();
+ Integer minSdk = mMinNameToApi.get(mValues.minSdk);
+ if (minSdk == null) {
+ try {
+ minSdk = Integer.parseInt(mValues.minSdk);
+ } catch (NumberFormatException nufe) {
+ // If not a number, then the string is a codename, so treat it
+ // as a preview version.
+ minSdk = SdkVersionInfo.HIGHEST_KNOWN_API + 1;
+ }
+ }
+ mValues.iconState.minSdk = minSdk.intValue();
+ mValues.minSdkLevel = minSdk.intValue();
+
+ // If higher than build target, adjust build target
+ if (mValues.minSdkLevel > mValues.getBuildApi()) {
+ // Try to find a build target with an adequate build API
+ IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
+ IAndroidTarget best = null;
+ int bestApi = Integer.MAX_VALUE;
+ int bestTargetIndex = -1;
+ for (int i = 0; i < targets.length; i++) {
+ IAndroidTarget target = targets[i];
+ if (!target.isPlatform()) {
+ continue;
+ }
+ int api = target.getVersion().getApiLevel();
+ if (api >= mValues.minSdkLevel && api < bestApi) {
+ best = target;
+ bestApi = api;
+ bestTargetIndex = i;
+ }
+ }
+
+ if (best != null) {
+ assert bestTargetIndex != -1;
+ mValues.target = best;
+ try {
+ mIgnore = true;
+ mBuildSdkCombo.select(bestTargetIndex);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ }
+
+ // If higher than targetSdkVersion, adjust targetSdkVersion
+ if (mValues.minSdkLevel > mValues.targetSdkLevel) {
+ mValues.targetSdkLevel = mValues.minSdkLevel;
+ try {
+ mIgnore = true;
+ setSelectedTargetSdk(mValues.targetSdkLevel);
+ } finally {
+ mIgnore = false;
+ }
+ }
+ } else if (source == mBuildSdkCombo) {
+ mValues.target = getSelectedBuildTarget();
+
+ // 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 == mTargetSdkCombo) {
+ mValues.targetSdkLevel = getSelectedTargetSdk();
+ }
+
+ 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 int getSelectedTargetSdk() {
+ // +1: First API level (at index 0) is 1
+ return mTargetSdkCombo.getSelectionIndex() + 1;
+ }
+
+ private void setSelectedMinSdk(int api) {
+ mMinSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
+ }
+
+ private void setSelectedTargetSdk(int api) {
+ mTargetSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1
+ }
+
+ @Nullable
+ private IAndroidTarget getSelectedBuildTarget() {
+ IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData();
+ 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);
+
+ String pkg = sb.toString();
+ if (pkg.endsWith(".")) { //$NON-NLS-1$
+ pkg = pkg.substring(0, pkg.length() - 1);
+ }
+ mValues.packageName = pkg;
+ 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 == '.')
+ && (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)) {
+ int length = SAMPLE_PACKAGE_PREFIX.length();
+ if (mPackageText.getText().length() > length
+ && SAMPLE_PACKAGE_PREFIX.endsWith(".")) { //$NON-NLS-1$
+ length--;
+ }
+ mPackageText.setSelection(0, length);
+ }
+ } else if (source == mTargetSdkCombo) {
+ tip = mTargetSdkDec.getDescriptionText();
+ } else if (source == mThemeCombo) {
+ tip = mThemeDec.getDescriptionText();
+ }
+ mTipLabel.setText(tip);
+ mHelpIcon.setVisible(tip.length() > 0);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ mTipLabel.setText("");
+ mHelpIcon.setVisible(false);
+ }
+
+ // Validation
+
+ private void validatePage() {
+ IStatus status = mValues.template.validateTemplate(mValues.minSdkLevel,
+ mValues.getBuildApi());
+ if (status != null && !status.isOK()) {
+ updateDecorator(mApplicationDec, null, true);
+ updateDecorator(mPackageDec, null, true);
+ updateDecorator(mProjectDec, null, true);
+ updateDecorator(mThemeDec, null, true);
+ /* These never get marked with errors:
+ updateDecorator(mBuildTargetDec, null, true);
+ updateDecorator(mMinSdkDec, null, true);
+ updateDecorator(mTargetSdkDec, null, true);
+ */
+ } else {
+ IStatus appStatus = validateAppName();
+ if (appStatus != null && (status == null
+ || appStatus.getSeverity() > status.getSeverity())) {
+ status = appStatus;
+ }
+
+ IStatus projectStatus = validateProjectName();
+ if (projectStatus != null && (status == null
+ || projectStatus.getSeverity() > status.getSeverity())) {
+ status = projectStatus;
+ }
+
+ IStatus packageStatus = validatePackageName();
+ if (packageStatus != null && (status == null
+ || packageStatus.getSeverity() > status.getSeverity())) {
+ status = packageStatus;
+ }
+
+ IStatus locationStatus = ProjectContentsPage.validateLocationInWorkspace(mValues);
+ if (locationStatus != null && (status == null
+ || locationStatus.getSeverity() > status.getSeverity())) {
+ status = locationStatus;
+ }
+
+ 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");
+ } else {
+ AndroidVersion version = mValues.target.getVersion();
+ if (version.isPreview()) {
+ if (version.getCodename().equals(mValues.minSdk) == false) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Preview platforms require the min SDK version to match their codenames.");
+ }
+ } else if (mValues.target.getVersion().compareTo(
+ mValues.minSdkLevel,
+ version.isPreview() ? mValues.minSdk : null) < 0) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ "The minimum SDK version is higher than the build target version");
+ }
+ if (status == null || status.getSeverity() != IStatus.ERROR) {
+ if (mValues.targetSdkLevel < mValues.minSdkLevel) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "The target SDK version should be at least as high as the minimum SDK version");
+ }
+ }
+ }
+ }
+
+ IStatus themeStatus = validateTheme();
+ if (themeStatus != null && (status == null
+ || themeStatus.getSeverity() > status.getSeverity())) {
+ status = themeStatus;
+ }
+ }
+
+ 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)) {
+ if (mValues.packageName != null
+ && !mValues.packageName.equals(SAMPLE_PACKAGE_PREFIX)) {
+ status = ApplicationInfoPage.validatePackage(mValues.packageName);
+ if (status == null || status.isOK()) {
+ status = new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID,
+ String.format("The prefix '%1$s' is meant as a placeholder and should " +
+ "not be used", SAMPLE_PACKAGE_PREFIX));
+ }
+ } else {
+ 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 IStatus validateTheme() {
+ IStatus status = null;
+
+ if (mThemeParameter != null) {
+ status = NewTemplatePage.validateCombo(null, mThemeParameter,
+ mThemeCombo.getSelectionIndex(), mValues.minSdkLevel,
+ mValues.getBuildApi());
+
+ updateDecorator(mThemeDec, 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 000000000..d350a00dd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java
@@ -0,0 +1,456 @@
+/*
+ * 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.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.VisibleForTesting;
+import com.android.assetstudiolib.GraphicGenerator;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+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.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator.ProjectPopulator;
+
+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.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Wizard for creating new projects
+ */
+public class NewProjectWizard extends TemplateWizard {
+ private static final String PARENT_ACTIVITY_CLASS = "parentActivityClass"; //$NON-NLS-1$
+ private static final String ACTIVITY_TITLE = "activityTitle"; //$NON-NLS-1$
+ static final String IS_LAUNCHER = "isLauncher"; //$NON-NLS-1$
+ static final String IS_NEW_PROJECT = "isNewProject"; //$NON-NLS-1$
+ static final String IS_LIBRARY_PROJECT = "isLibraryProject"; //$NON-NLS-1$
+ static final String ATTR_COPY_ICONS = "copyIcons"; //$NON-NLS-1$
+ static final String ATTR_TARGET_API = "targetApi"; //$NON-NLS-1$
+ static final String ATTR_MIN_API = "minApi"; //$NON-NLS-1$
+ static final String ATTR_MIN_BUILD_API = "minBuildApi"; //$NON-NLS-1$
+ static final String ATTR_BUILD_API = "buildApi"; //$NON-NLS-1$
+ static final String ATTR_REVISION = "revision"; //$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$
+ static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$
+ static final String CATEGORY_ACTIVITIES = "activities"; //$NON-NLS-1$
+ static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$
+ static final String ATTR_APP_COMPAT = "appCompat"; //$NON-NLS-1$
+ /**
+ * Reserved file name for the launcher icon, resolves to the xhdpi version
+ *
+ * @see CreateAssetSetWizardState#getImage
+ */
+ public static final String DEFAULT_LAUNCHER_ICON = "launcher_icon"; //$NON-NLS-1$
+
+ private NewProjectPage mMainPage;
+ private ProjectContentsPage mContentsPage;
+ private ActivityPage mActivityPage;
+ private NewTemplatePage mTemplatePage;
+ private NewProjectWizardState mValues;
+ /** The project being created */
+ private IProject mProject;
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ setWindowTitle("New Android Application");
+
+ mValues = new NewProjectWizardState();
+ mMainPage = new NewProjectPage(mValues);
+ mContentsPage = new ProjectContentsPage(mValues);
+ mContentsPage.init(selection, AdtUtils.getActivePart());
+ mActivityPage = new ActivityPage(mValues, true, true);
+ mActivityPage.setLauncherActivitiesOnly(true);
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mMainPage);
+ addPage(mContentsPage);
+ addPage(mActivityPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ if (page == mMainPage) {
+ return mContentsPage;
+ }
+
+ if (page == mContentsPage) {
+ if (mValues.createIcon) {
+ // 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.trim = true;
+
+ // ADT 20: White icon with blue shape
+ //iconState.shape = GraphicGenerator.Shape.CIRCLE;
+ //iconState.sourceType = CreateAssetSetWizardState.SourceType.CLIPART;
+ //iconState.clipartName = "user.png"; //$NON-NLS-1$
+ //iconState.padding = 10;
+
+ // ADT 21: Use the platform packaging icon, but allow user to customize it
+ iconState.sourceType = CreateAssetSetWizardState.SourceType.IMAGE;
+ iconState.imagePath = new File(DEFAULT_LAUNCHER_ICON);
+ iconState.shape = GraphicGenerator.Shape.NONE;
+ iconState.padding = 0;
+
+ WizardPage p = getIconPage(mValues.iconState);
+ p.setTitle("Configure Launcher Icon");
+ return p;
+ } else {
+ if (mValues.createActivity) {
+ return mActivityPage;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ if (page == mIconPage) {
+ return mActivityPage;
+ }
+
+ 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);
+ hidden.add(ATTR_BUILD_API);
+ hidden.add(IS_LAUNCHER);
+ // Don't ask about hierarchical parent activities in new projects where there
+ // can't possibly be any
+ hidden.add(PARENT_ACTIVITY_CLASS);
+ hidden.add(ACTIVITY_TITLE); // Not used for the first activity in the project
+
+ mTemplatePage = new NewTemplatePage(activityValues, false);
+ addPage(mTemplatePage);
+ }
+ mTemplatePage.setCustomMinSdk(mValues.minSdkLevel, mValues.getBuildApi());
+ return mTemplatePage;
+ }
+
+ if (page == mTemplatePage) {
+ TemplateMetadata template = mValues.activityValues.getTemplateHandler().getTemplate();
+ if (template != null
+ && !InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getDependencyPage(template, true);
+ }
+ }
+
+ if (page == mTemplatePage || !mValues.createActivity && page == mActivityPage
+ || page == getDependencyPage(null, false)) {
+ return null;
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ public boolean canFinish() {
+ // 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
+ @NonNull
+ protected IProject getProject() {
+ return mProject;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ return mValues.template.getFilesToOpen();
+ }
+
+ @VisibleForTesting
+ NewProjectWizardState getValues() {
+ return mValues;
+ }
+
+ @VisibleForTesting
+ void setValues(NewProjectWizardState values) {
+ mValues = values;
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ final TemplateHandler template = mValues.template;
+ // 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.
+ // (NOTE: After the change from direct file manipulation to creating a list of Change
+ // objects, this no longer applies - but the code is kept around a little while longer
+ // in case we want to generate change objects that makes backups of merged files)
+ template.setBackupMergedFiles(false);
+
+ // Generate basic output skeleton
+ Map<String, Object> paramMap = new HashMap<String, Object>();
+ addProjectInfo(paramMap);
+ TemplateHandler.addDirectoryParameters(paramMap, getProject());
+ // We don't know at this point whether the activity is going to need
+ // AppCompat so we just assume that it will.
+ if (mValues.createActivity && mValues.minSdkLevel < 14) {
+ paramMap.put(ATTR_APP_COMPAT, true);
+ getFinalizingActions().add(new Runnable() {
+ @Override
+ public void run() {
+ AddSupportJarAction.installAppCompatLibrary(mProject, true);
+ }
+ });
+ }
+
+ return template.render(mProject, paramMap);
+ }
+
+ @Override
+ protected boolean performFinish(final IProgressMonitor monitor)
+ throws InvocationTargetException {
+ try {
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ String name = mValues.projectName;
+ mProject = 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);
+
+ ProjectPopulator projectPopulator = new ProjectPopulator() {
+ @Override
+ public void populate(IProject project) throws InvocationTargetException {
+ // Copy in the proguard file; templates don't provide this one.
+ // add the default proguard config
+ File libFolder = new File(AdtPlugin.getOsSdkToolsFolder(),
+ SdkConstants.FD_LIB);
+ try {
+ assert project == mProject;
+ NewProjectCreator.addLocalFile(project,
+ new File(libFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE),
+ // Write ProGuard config files with the extension .pro which
+ // is what is used in the ProGuard documentation and samples
+ SdkConstants.FN_PROJECT_PROGUARD_FILE,
+ new NullProgressMonitor());
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+
+ try {
+ mProject.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ // Render the project template
+ List<Change> changes = computeChanges();
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating project...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ if (mValues.createIcon) { // TODO: Set progress
+ generateIcons(mProject);
+ }
+
+ // Render the embedded activity template template
+ if (mValues.createActivity) {
+ final TemplateHandler activityTemplate =
+ mValues.activityValues.getTemplateHandler();
+ // 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.
+ activityTemplate.setBackupMergedFiles(false);
+ generateActivity(template, project, monitor);
+ }
+ }
+ };
+
+ NewProjectCreator.create(monitor, mProject, mValues.target, projectPopulator,
+ mValues.isLibrary, mValues.projectLocation, mValues.workingSets);
+
+ // For new projects, ensure that we're actually using the preferred compliance,
+ // not just the default one
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject != null) {
+ ProjectHelper.enforcePreferredCompilerCompliance(javaProject);
+ }
+
+ try {
+ mProject.refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ List<Runnable> finalizingTasks = getFinalizingActions();
+ for (Runnable r : finalizingTasks) {
+ r.run();
+ }
+
+ 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;
+ ConfigureAssetSetPage.generateIcons(newProject, mValues.iconState, false, mIconPage);
+ }
+
+ /**
+ * 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(TemplateHandler projectTemplate, IProject project,
+ IProgressMonitor monitor) throws InvocationTargetException {
+ assert mValues.createActivity;
+ NewTemplateWizardState activityValues = mValues.activityValues;
+ Map<String, Object> parameters = activityValues.parameters;
+
+ addProjectInfo(parameters);
+
+ parameters.put(IS_NEW_PROJECT, true);
+ parameters.put(IS_LIBRARY_PROJECT, mValues.isLibrary);
+ // Ensure that activities created as part of a new project are marked as
+ // launcher activities
+ parameters.put(IS_LAUNCHER, true);
+ TemplateHandler.addDirectoryParameters(parameters, project);
+
+ TemplateHandler activityTemplate = activityValues.getTemplateHandler();
+ activityTemplate.setBackupMergedFiles(false);
+ List<Change> changes = activityTemplate.render(project, parameters);
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating template...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ List<String> filesToOpen = activityTemplate.getFilesToOpen();
+ projectTemplate.getFilesToOpen().addAll(filesToOpen);
+
+ List<Runnable> finalizingActions = activityTemplate.getFinalizingActions();
+ projectTemplate.getFinalizingActions().addAll(finalizingActions);
+ }
+
+ private void addProjectInfo(Map<String, Object> parameters) {
+ parameters.put(ATTR_PACKAGE_NAME, mValues.packageName);
+ parameters.put(ATTR_APP_TITLE, mValues.applicationName);
+ parameters.put(ATTR_MIN_API, mValues.minSdk);
+ parameters.put(ATTR_MIN_API_LEVEL, mValues.minSdkLevel);
+ parameters.put(ATTR_TARGET_API, mValues.targetSdkLevel);
+ parameters.put(ATTR_BUILD_API, mValues.target.getVersion().getApiLevel());
+ parameters.put(ATTR_COPY_ICONS, !mValues.createIcon);
+ parameters.putAll(mValues.parameters);
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ return mValues.template.getFinalizingActions();
+ }
+}
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 000000000..9cd3a6dcf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizardState.java
@@ -0,0 +1,125 @@
+/*
+ * 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.CATEGORY_PROJECTS;
+
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.ui.IWorkingSet;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Value object which holds the current state of the wizard pages for the
+ * {@link NewProjectWizard}
+ */
+public class NewProjectWizardState {
+ /** Creates a new {@link NewProjectWizardState} */
+ public NewProjectWizardState() {
+ template = TemplateHandler.createFromName(CATEGORY_PROJECTS,
+ "NewAndroidApplication"); //$NON-NLS-1$
+ }
+
+ /** 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, as a string (if the API is a preview release with a codename) */
+ public String minSdk;
+
+ /** The minimum SDK API level to use */
+ public int minSdkLevel;
+
+ /** The target SDK level */
+ public int targetSdkLevel = AdtUtils.getHighestKnownApiLevel();
+
+ /** Whether this project should be marked as a library project */
+ public boolean isLibrary;
+
+ /** Whether to create an activity (if so, the activity state is stored in
+ * {@link #activityValues}) */
+ public boolean createActivity = 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();
+
+ /** Whether a custom location should be used */
+ public boolean useDefaultLocation = true;
+
+ /** Folder where the project should be created. */
+ public String projectLocation;
+
+ /** Configured parameters, by id */
+ public final Map<String, Object> parameters = new HashMap<String, Object>();
+
+ /** The set of chosen working sets to use when creating the project */
+ public IWorkingSet[] workingSets = new IWorkingSet[0];
+
+ /**
+ * Returns the build target API level
+ *
+ * @return the build target API level
+ */
+ public int getBuildApi() {
+ return target != null ? target.getVersion().getApiLevel() : minSdkLevel;
+ }
+}
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 000000000..57cf5c824
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java
@@ -0,0 +1,946 @@
+/*
+ * 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.SdkConstants.CLASS_ACTIVITY;
+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_BUILD_API;
+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.NonNull;
+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.ImageControl;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo;
+import com.android.ide.eclipse.adt.internal.wizards.templates.Parameter.Constraint;
+import com.android.ide.eclipse.adt.internal.wizards.templates.Parameter.Type;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.ui.IJavaElementSearchConstants;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
+import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+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.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.ByteArrayInputStream;
+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 int mCustomMinSdk = -1;
+ private int mCustomBuildApi = -1;
+ 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 boolean mDisposePreviewImage;
+ private ProjectCombo mProjectButton;
+ private StringEvaluator mEvaluator;
+
+ private TemplateMetadata mShowingTemplate;
+
+ /**
+ * Creates a new {@link NewTemplatePage}
+ *
+ * @param values the wizard state
+ * @param chooseProject whether the wizard should present a project chooser,
+ * and update {@code values}' project field
+ */
+ NewTemplatePage(NewTemplateWizardState values, boolean chooseProject) {
+ super("newTemplatePage"); //$NON-NLS-1$
+ mValues = values;
+ mChooseProject = chooseProject;
+ }
+
+ /**
+ * @param minSdk a minimum SDK to use, provided chooseProject is false. If
+ * it is true, then the minimum SDK used for validation will be
+ * the one of the project
+ * @param buildApi the build API to use
+ */
+ void setCustomMinSdk(int minSdk, int buildApi) {
+ assert !mChooseProject;
+ //assert buildApi >= minSdk;
+ mCustomMinSdk = minSdk;
+ mCustomBuildApi = buildApi;
+ }
+
+ @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() {
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+ if (template == mShowingTemplate) {
+ return;
+ }
+ mShowingTemplate = template;
+
+ 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;
+ String thumb = null;
+ if (template != null) {
+ thumb = template.getThumbnailPath();
+ String title = template.getTitle();
+ if (title != null && !title.isEmpty()) {
+ setTitle(title);
+ }
+ String description = template.getDescription();
+ if (description != null && !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();
+ 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;
+ Object value = defaults.get(id);
+ if (value == null) {
+ value = parameter.value;
+ }
+
+ 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;
+ }
+
+ switch (type) {
+ case 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;
+
+ if (parameter.constraints.contains(Constraint.EXISTS)) {
+ text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 1, 1));
+
+ Button button = new Button(container, SWT.FLAT);
+ button.setData(parameter);
+ button.setText("...");
+ button.addSelectionListener(this);
+ } else {
+ text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 2, 1));
+ }
+
+ boolean hasValue = false;
+ if (value instanceof String) {
+ String stringValue = (String) value;
+ hasValue = !stringValue.isEmpty();
+ text.setText(stringValue);
+ mValues.parameters.put(id, value);
+ }
+
+ if (!hasValue) {
+ if (parameter.constraints.contains(Constraint.EMPTY)) {
+ text.setMessage("Optional");
+ } else if (parameter.constraints.contains(Constraint.NONEMPTY)) {
+ text.setMessage("Required");
+ }
+ }
+
+ 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);
+ }
+ break;
+ }
+ case 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 instanceof Boolean) {
+ Boolean selected = (Boolean) 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);
+ }
+ break;
+ }
+ case 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 = createOptionCombo(parameter, container, mValues.parameters,
+ this, this);
+ combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,
+ 2, 1));
+
+ if (mFirst == null) {
+ mFirst = combo;
+ }
+
+ if (help != null && !help.isEmpty()) {
+ ControlDecoration decoration = createFieldDecoration(id, combo, help);
+ }
+ break;
+ }
+ case SEPARATOR:
+ // Already handled above
+ assert false : type;
+ break;
+ default:
+ assert false : type;
+ }
+ }
+ }
+
+ // Preview
+ mPreview = new ImageControl(parent, SWT.NONE, null);
+ mPreview.setDisposeImage(false); // Handled manually in this class
+ 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));
+
+ 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();
+ }
+
+ @NonNull
+ static Combo createOptionCombo(
+ @NonNull Parameter parameter,
+ @NonNull Composite container,
+ @NonNull Map<String, Object> valueMap,
+ @NonNull SelectionListener selectionListener,
+ @NonNull FocusListener focusListener) {
+ Combo combo = new Combo(container, SWT.READ_ONLY);
+
+ List<Element> options = parameter.getOptions();
+ assert options.size() > 0;
+ int selected = 0;
+ List<String> ids = Lists.newArrayList();
+ List<Integer> minSdks = Lists.newArrayList();
+ List<Integer> minBuildApis = 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();
+
+ String minApiString = option.getAttribute(ATTR_MIN_API);
+ int minSdk = 1;
+ if (minApiString != null && !minApiString.isEmpty()) {
+ try {
+ minSdk = Integer.parseInt(minApiString);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should
+ // always be an integer
+ AdtPlugin.log(nufe, null);
+ minSdk = 1;
+ }
+ }
+ String minBuildApiString = option.getAttribute(ATTR_MIN_BUILD_API);
+ int minBuildApi = 1;
+ if (minBuildApiString != null && !minBuildApiString.isEmpty()) {
+ try {
+ minBuildApi = Integer.parseInt(minBuildApiString);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should
+ // always be an integer
+ AdtPlugin.log(nufe, null);
+ minBuildApi = 1;
+ }
+ }
+ minSdks.add(minSdk);
+ minBuildApis.add(minBuildApi);
+ ids.add(optionId);
+ labels.add(optionLabel);
+ }
+ combo.setData(parameter);
+ parameter.control = combo;
+ combo.setData(ATTR_ID, ids.toArray(new String[ids.size()]));
+ combo.setData(ATTR_MIN_API, minSdks.toArray(new Integer[minSdks.size()]));
+ combo.setData(ATTR_MIN_BUILD_API, minBuildApis.toArray(
+ new Integer[minBuildApis.size()]));
+ assert labels.size() > 0;
+ combo.setItems(labels.toArray(new String[labels.size()]));
+ combo.select(selected);
+
+ combo.addSelectionListener(selectionListener);
+ combo.addFocusListener(focusListener);
+
+ valueMap.put(parameter.id, ids.get(selected));
+
+ if (parameter.help != null && !parameter.help.isEmpty()) {
+ combo.setToolTipText(parameter.help);
+ }
+
+ return combo;
+ }
+
+ private void setPreview(String thumb) {
+ Image oldImage = mPreviewImage;
+ boolean dispose = mDisposePreviewImage;
+ mPreviewImage = null;
+
+ if (thumb == null || thumb.isEmpty()) {
+ mPreviewImage = TemplateMetadata.getDefaultTemplateIcon();
+ mDisposePreviewImage = false;
+ } else {
+ byte[] data = mValues.getTemplateHandler().readTemplateResource(thumb);
+ if (data != null) {
+ try {
+ mPreviewImage = new Image(getControl().getDisplay(),
+ new ByteArrayInputStream(data));
+ mDisposePreviewImage = true;
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ if (mPreviewImage == null) {
+ return;
+ }
+ }
+
+ mPreview.setImage(mPreviewImage);
+ mPreview.fitToWidth(PREVIEW_WIDTH);
+
+ if (oldImage != null && dispose) {
+ oldImage.dispose();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mPreviewImage != null && mDisposePreviewImage) {
+ mDisposePreviewImage = false;
+ 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 && super.isPageComplete();
+ }
+
+ @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
+ static Parameter getParameter(Control control) {
+ return (Parameter) control.getData();
+ }
+
+ /**
+ * Returns the current string evaluator, if any
+ *
+ * @return the evaluator or null
+ */
+ @Nullable
+ public StringEvaluator getEvaluator() {
+ return mEvaluator;
+ }
+
+ // ---- Validation ----
+
+ private void validatePage() {
+ int minSdk = getMinSdk();
+ int buildApi = getBuildApi();
+ IStatus status = mValues.getTemplateHandler().validateTemplate(minSdk, buildApi);
+
+ if (status == null || status.isOK()) {
+ if (mChooseProject && mValues.project == null) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Please select an Android project.");
+ }
+ }
+
+ for (Parameter parameter : mShowingTemplate.getParameters()) {
+ if (parameter.type == Parameter.Type.SEPARATOR) {
+ continue;
+ }
+ 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) {
+ IStatus s = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, error);
+ if (decoration != null) {
+ updateDecorator(decoration, s, parameter.help);
+ }
+ if (status == null || status.isOK()) {
+ status = s;
+ }
+ } else if (decoration != null) {
+ updateDecorator(decoration, null, parameter.help);
+ }
+ }
+
+ if (status == null || status.isOK()) {
+ if (parameter.control instanceof Combo) {
+ status = validateCombo(status, parameter, minSdk, buildApi);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ /** Validates the given combo */
+ static IStatus validateCombo(IStatus status, Parameter parameter, int minSdk, int buildApi) {
+ Combo combo = (Combo) parameter.control;
+ int index = combo.getSelectionIndex();
+ return validateCombo(status, parameter, index, minSdk, buildApi);
+ }
+
+ /** Validates the given combo assuming the value at the given index is chosen */
+ static IStatus validateCombo(IStatus status, Parameter parameter, int index,
+ int minSdk, int buildApi) {
+ Combo combo = (Combo) parameter.control;
+ Integer[] optionIds = (Integer[]) combo.getData(ATTR_MIN_API);
+ // Check minSdk
+ if (index != -1 && index < optionIds.length) {
+ Integer requiredMinSdk = optionIds[index];
+ if (requiredMinSdk > minSdk) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "%1$s \"%2$s\" requires a minimum SDK version of at " +
+ "least %3$d, and the current min version is %4$d",
+ parameter.name, combo.getItems()[index], requiredMinSdk, minSdk));
+ }
+ }
+
+ // Check minimum build target
+ optionIds = (Integer[]) combo.getData(ATTR_MIN_BUILD_API);
+ if (index != -1 && index < optionIds.length) {
+ Integer requiredBuildApi = optionIds[index];
+ if (requiredBuildApi > buildApi) {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ "%1$s \"%2$s\" requires a build target API version of at " +
+ "least %3$d, and the current version is %4$d",
+ parameter.name, combo.getItems()[index], requiredBuildApi, buildApi));
+ }
+ }
+ return status;
+ }
+
+ private int getMinSdk() {
+ return mChooseProject ? mValues.getMinSdk() : mCustomMinSdk;
+ }
+
+ private int getBuildApi() {
+ return mChooseProject ? mValues.getBuildApi() : mCustomBuildApi;
+ }
+
+ 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();
+ if (template != null) {
+ setPreview(template.getThumbnailPath());
+ }
+ }
+ } else if (source instanceof Button) {
+ Button button = (Button) source;
+ Parameter parameter = (Parameter) button.getData();
+ if (parameter.type == Type.BOOLEAN) {
+ // Checkbox parameter
+ editParameter(button, button.getSelection());
+
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+ if (template != null) {
+ setPreview(template.getThumbnailPath());
+ }
+ } else {
+ // Choose button for some other parameter, usually a text
+ String activity = chooseActivity();
+ if (activity != null) {
+ setValue(parameter, activity);
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ private String chooseActivity() {
+ try {
+ // Compute a search scope: We need to merge all the subclasses
+ // android.app.Fragment and android.support.v4.app.Fragment
+ IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
+ IProject project = mValues.project;
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType activityType = null;
+
+ if (javaProject != null) {
+ activityType = javaProject.findType(CLASS_ACTIVITY);
+ }
+ if (activityType == null) {
+ IJavaProject[] projects = BaseProjectHelper.getAndroidProjects(null);
+ for (IJavaProject p : projects) {
+ activityType = p.findType(CLASS_ACTIVITY);
+ if (activityType != null) {
+ break;
+ }
+ }
+ }
+ if (activityType != null) {
+ NullProgressMonitor monitor = new NullProgressMonitor();
+ ITypeHierarchy hierarchy = activityType.newTypeHierarchy(monitor);
+ IType[] classes = hierarchy.getAllSubtypes(activityType);
+ scope = SearchEngine.createJavaSearchScope(classes, IJavaSearchScope.SOURCES);
+ }
+
+ Shell parent = AdtPlugin.getShell();
+ final SelectionDialog dialog = JavaUI.createTypeDialog(
+ parent,
+ new ProgressMonitorDialog(parent),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, false,
+ // Use ? as a default filter to fill dialog with matches
+ "?", //$NON-NLS-1$
+ new TypeSelectionExtension() {
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ @Override
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+ int modifiers = typeInfoRequestor.getModifiers();
+ if (!Flags.isPublic(modifiers)
+ || Flags.isInterface(modifiers)
+ || Flags.isEnum(modifiers)) {
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+ });
+
+ dialog.setTitle("Choose Activity Class");
+ dialog.setMessage("Select an Activity class (? = any character, * = any string):");
+ if (dialog.open() == IDialogConstants.CANCEL_ID) {
+ return null;
+ }
+
+ Object[] types = dialog.getResult();
+ if (types != null && types.length > 0) {
+ return ((IType) types[0]).getFullyQualifiedName();
+ }
+ } catch (JavaModelException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return null;
+ }
+
+ 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
+ List<Parameter> parameters = mShowingTemplate.getParameters();
+ for (Parameter p : parameters) {
+ if (p == parameter || p.suggest == null || p.edited ||
+ p.type == Parameter.Type.SEPARATOR) {
+ continue;
+ }
+ if (!p.suggest.contains(id)) {
+ continue;
+ }
+
+ try {
+ if (mEvaluator == null) {
+ mEvaluator = new StringEvaluator();
+ }
+ String updated = mEvaluator.evaluate(p.suggest, parameters);
+ if (updated != null && !updated.equals(p.value)) {
+ setValue(p, updated);
+ }
+ } catch (Throwable t) {
+ // Pass: Ignore updating if something wrong happens
+ t.printStackTrace(); // during development only
+ }
+ }
+ }
+ }
+
+ private void setValue(Parameter p, String value) {
+ p.value = value;
+ mValues.parameters.put(p.id, value);
+
+ // Update form widgets
+ boolean prevIgnore = mIgnore;
+ try {
+ mIgnore = true;
+ if (p.control instanceof Text) {
+ ((Text) p.control).setText(value);
+ } 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;
+ }
+ }
+
+ @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 000000000..99814f731
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizard.java
@@ -0,0 +1,212 @@
+/*
+ * 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_BUILD_API;
+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 com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+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.Set;
+
+/**
+ * Template wizard which creates parameterized templates
+ */
+public class NewTemplateWizard extends TemplateWizard {
+ /** Template name and location under $sdk/templates for the default activity */
+ static final String BLANK_ACTIVITY = "activities/BlankActivity"; //$NON-NLS-1$
+ /** Template name and location under $sdk/templates for the custom view template */
+ static final String CUSTOM_VIEW = "other/CustomView"; //$NON-NLS-1$
+
+ protected NewTemplatePage mMainPage;
+ protected NewTemplateWizardState mValues;
+ private final String mTemplateName;
+
+ NewTemplateWizard(String templateName) {
+ mTemplateName = templateName;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ super.init(workbench, selection);
+
+ mValues = new NewTemplateWizardState();
+
+ File template = TemplateManager.getTemplateLocation(mTemplateName);
+ if (template != null) {
+ mValues.setTemplateLocation(template);
+ }
+ hideBuiltinParameters();
+
+ List<IProject> projects = AdtUtils.getSelectedProjects(selection);
+ if (projects.size() == 1) {
+ mValues.project = projects.get(0);
+ }
+
+ mMainPage = new NewTemplatePage(mValues, true);
+ }
+
+ @Override
+ protected boolean shouldAddIconPage() {
+ return mValues.getIconState() != null;
+ }
+
+ /**
+ * 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);
+ hidden.add(ATTR_BUILD_API);
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ addPage(mMainPage);
+ }
+
+ @Override
+ public IWizardPage getNextPage(IWizardPage page) {
+ TemplateMetadata template = mValues.getTemplateHandler().getTemplate();
+
+ if (page == mMainPage && shouldAddIconPage()) {
+ WizardPage iconPage = getIconPage(mValues.getIconState());
+ mValues.updateIconState(mMainPage.getEvaluator());
+ return iconPage;
+ } else if (page == mMainPage
+ || shouldAddIconPage() && page == getIconPage(mValues.getIconState())) {
+ if (template != null) {
+ if (InstallDependencyPage.isInstalled(template.getDependencies())) {
+ return getPreviewPage(mValues);
+ } else {
+ return getDependencyPage(template, true);
+ }
+ }
+ } else if (page == getDependencyPage(template, false)) {
+ return getPreviewPage(mValues);
+ }
+
+ return super.getNextPage(page);
+ }
+
+ @Override
+ @NonNull
+ protected IProject getProject() {
+ return mValues.project;
+ }
+
+ @Override
+ @NonNull
+ protected List<String> getFilesToOpen() {
+ TemplateHandler activityTemplate = mValues.getTemplateHandler();
+ return activityTemplate.getFilesToOpen();
+ }
+
+ @Override
+ @NonNull
+ protected List<Runnable> getFinalizingActions() {
+ TemplateHandler activityTemplate = mValues.getTemplateHandler();
+ return activityTemplate.getFinalizingActions();
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ return mValues.computeChanges();
+ }
+
+ /**
+ * 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 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 000000000..2c97003f2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java
@@ -0,0 +1,215 @@
+/*
+ * 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_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_COPY_ICONS;
+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 com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LIBRARY_PROJECT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_NEW_PROJECT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard.BLANK_ACTIVITY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+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 {
+ /** 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;
+
+ /** The minimum API level to use for this template */
+ public int minSdkLevel;
+
+ /** Location of the template being created */
+ private File mTemplateLocation;
+
+ /**
+ * State for the asset studio wizard, used to create custom icons provided
+ * the icon requests it with an {@code <icons>} element
+ */
+ private CreateAssetSetWizardState mIconState;
+
+ /**
+ * Create a new state object for use by the {@link NewTemplatePage}
+ */
+ public NewTemplateWizardState() {
+ parameters.put(IS_NEW_PROJECT, false);
+ }
+
+ @NonNull
+ TemplateHandler getTemplateHandler() {
+ if (mTemplateHandler == null) {
+ File inputPath;
+ if (mTemplateLocation != null) {
+ inputPath = mTemplateLocation;
+ } else {
+ // Default
+ inputPath = TemplateManager.getTemplateLocation(BLANK_ACTIVITY);
+ }
+ mTemplateHandler = TemplateHandler.createFromPath(inputPath);
+ }
+
+ return mTemplateHandler;
+ }
+
+ /** Sets the current template */
+ void setTemplateLocation(File file) {
+ if (!file.equals(mTemplateLocation)) {
+ mTemplateLocation = file;
+ mTemplateHandler = null;
+ }
+ }
+
+ /** Returns the current template */
+ File getTemplateLocation() {
+ return mTemplateLocation;
+ }
+
+ /** Returns the min SDK version to use */
+ int getMinSdk() {
+ if (project == null) {
+ return -1;
+ }
+ ManifestInfo manifest = ManifestInfo.get(project);
+ return manifest.getMinSdkVersion();
+ }
+
+ /** Returns the build API version to use */
+ int getBuildApi() {
+ if (project == null) {
+ return -1;
+ }
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+ if (target != null) {
+ return target.getVersion().getApiLevel();
+ }
+
+ return getMinSdk();
+ }
+
+ /** Computes the changes this wizard will make */
+ @NonNull
+ List<Change> computeChanges() {
+ if (project == null) {
+ return Collections.emptyList();
+ }
+
+ ManifestInfo manifest = ManifestInfo.get(project);
+ parameters.put(ATTR_PACKAGE_NAME, manifest.getPackage());
+ parameters.put(ATTR_MIN_API, manifest.getMinSdkName());
+ parameters.put(ATTR_MIN_API_LEVEL, manifest.getMinSdkVersion());
+ parameters.put(ATTR_TARGET_API, manifest.getTargetSdkVersion());
+ parameters.put(ATTR_BUILD_API, getBuildApi());
+ parameters.put(ATTR_COPY_ICONS, mIconState == null);
+ ProjectState projectState = Sdk.getProjectState(project);
+ parameters.put(IS_LIBRARY_PROJECT,
+ projectState != null ? projectState.isLibrary() : false);
+
+ TemplateHandler.addDirectoryParameters(parameters, project);
+
+ List<Change> changes = getTemplateHandler().render(project, parameters);
+
+ if (mIconState != null) {
+ String title = String.format("Generate icons (res/drawable-<density>/%1$s.png)",
+ mIconState.outputName);
+ changes.add(new NullChange(title) {
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ ConfigureAssetSetPage.generateIcons(mIconState.project,
+ mIconState, false, null);
+
+ // Not undoable: just return null instead of an undo-change.
+ return null;
+ }
+ });
+
+ }
+
+ return changes;
+ }
+
+ @NonNull
+ CreateAssetSetWizardState getIconState() {
+ if (mIconState == null) {
+ TemplateHandler handler = getTemplateHandler();
+ if (handler != null) {
+ TemplateMetadata template = handler.getTemplate();
+ if (template != null) {
+ mIconState = template.getIconState(project);
+ }
+ }
+ }
+
+ return mIconState;
+ }
+
+ /**
+ * Updates the icon state, such as the output name, based on other parameter settings
+ * @param evaluator the string evaluator, or null if none exists
+ */
+ public void updateIconState(@Nullable StringEvaluator evaluator) {
+ TemplateMetadata template = getTemplateHandler().getTemplate();
+ if (template != null) {
+ if (evaluator == null) {
+ evaluator = new StringEvaluator();
+ }
+ template.updateIconName(template.getParameters(), evaluator);
+ }
+ }
+}
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 000000000..3139451c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java
@@ -0,0 +1,417 @@
+/*
+ * 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_PACKAGE_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CONSTRAINTS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_HELP;
+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.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.google.common.base.Splitter;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.swt.widgets.Control;
+import org.w3c.dom.Element;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+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 is allowed to be empty */
+ EMPTY,
+
+ /** The associated value should represent a fully qualified activity class name */
+ ACTIVITY,
+
+ /** 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 template defining the parameter */
+ public final TemplateMetadata template;
+
+ /** 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 TemplateMetadata template, @NonNull Element parameter) {
+ this.template = template;
+ 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);
+ }
+
+ if (initial != null && !initial.isEmpty() && type == Type.BOOLEAN) {
+ value = Boolean.valueOf(initial);
+ } else {
+ value = initial;
+ }
+ }
+
+ Parameter(
+ @NonNull TemplateMetadata template,
+ @NonNull Type type,
+ @NonNull String id,
+ @NonNull String initialValue) {
+ this.template = template;
+ this.type = type;
+ this.id = id;
+ this.value = initialValue;
+ element = null;
+ initial = null;
+ suggest = null;
+ name = id;
+ help = null;
+ constraints = EnumSet.noneOf(Constraint.class);
+ }
+
+ List<Element> getOptions() {
+ if (element != null) {
+ return DomUtilities.getChildren(element);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ @Nullable
+ public IInputValidator getValidator(@Nullable final 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.PACKAGE)
+ || constraints.contains(Constraint.CLASS)
+ || constraints.contains(Constraint.ACTIVITY)) {
+ mValidator = new IInputValidator() {
+ @Override
+ public String isValid(String newText) {
+ newText = newText.trim();
+ if (newText.isEmpty()) {
+ if (constraints.contains(Constraint.EMPTY)) {
+ return null;
+ } else if (constraints.contains(Constraint.NONEMPTY)) {
+ return String.format("Enter a value for %1$s", name);
+ } else {
+ // Compatibility mode: older templates might not specify;
+ // in that case, accept empty
+ if (!"activityClass".equals(id)) { //$NON-NLS-1$
+ return null;
+ }
+ }
+ }
+ IStatus status;
+ if (constraints.contains(Constraint.ACTIVITY)) {
+ status = ApplicationInfoPage.validateActivity(newText);
+ } else if (constraints.contains(Constraint.PACKAGE)) {
+ status = ApplicationInfoPage.validatePackage(newText);
+ } else {
+ assert constraints.contains(Constraint.CLASS);
+ status = ApplicationInfoPage.validateClass(newText);
+ }
+ if (status != null && !status.isOK()) {
+ return status.getMessage();
+ }
+
+ // Uniqueness
+ if (project != null && constraints.contains(Constraint.UNIQUE)) {
+ try {
+ // Determine the package.
+ // If there is a package info
+
+ IJavaProject p = BaseProjectHelper.getJavaProject(project);
+ if (p != null) {
+ String fqcn = newText;
+ if (fqcn.indexOf('.') == -1) {
+ String pkg = null;
+ Parameter parameter = template.getParameter(
+ ATTR_PACKAGE_NAME);
+ if (parameter != null && parameter.value != null) {
+ pkg = parameter.value.toString();
+ } else {
+ pkg = ManifestInfo.get(project).getPackage();
+ }
+ fqcn = pkg.isEmpty() ? newText : pkg + '.' + newText;
+ }
+
+ IType t = p.findType(fqcn);
+ if (t != null && t.exists()) {
+ return String.format("%1$s already exists", newText);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+ };
+ 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/ProjectContentsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java
new file mode 100644
index 000000000..7d7881fcf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ProjectContentsPage.java
@@ -0,0 +1,380 @@
+/*
+ * 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 com.android.ide.eclipse.adt.internal.wizards.newproject.WorkingSetGroup;
+import com.android.ide.eclipse.adt.internal.wizards.newproject.WorkingSetHelper;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.IWizardPage;
+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.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.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkingSet;
+
+import java.io.File;
+
+/**
+ * Second wizard page in the "New Project From Template" wizard
+ */
+public class ProjectContentsPage extends WizardPage
+ implements ModifyListener, SelectionListener, FocusListener {
+
+ private final NewProjectWizardState mValues;
+
+ private boolean mIgnore;
+ private Button mCustomIconToggle;
+ private Button mLibraryToggle;
+
+ private Button mUseDefaultLocationToggle;
+ private Label mLocationLabel;
+ private Text mLocationText;
+ private Button mChooseLocationButton;
+ private static String sLastProjectLocation = System.getProperty("user.home"); //$NON-NLS-1$
+ private Button mCreateActivityToggle;
+ private WorkingSetGroup mWorkingSetGroup;
+
+ ProjectContentsPage(NewProjectWizardState values) {
+ super("newAndroidApp"); //$NON-NLS-1$
+ mValues = values;
+ setTitle("New Android Application");
+ setDescription("Configure Project");
+
+ mWorkingSetGroup = new WorkingSetGroup();
+ setWorkingSets(new IWorkingSet[0]);
+ }
+
+ @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);
+
+ mCustomIconToggle = new Button(container, SWT.CHECK);
+ mCustomIconToggle.setSelection(true);
+ mCustomIconToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mCustomIconToggle.setText("Create custom launcher icon");
+ mCustomIconToggle.setSelection(mValues.createIcon);
+ mCustomIconToggle.addSelectionListener(this);
+
+ mCreateActivityToggle = new Button(container, SWT.CHECK);
+ mCreateActivityToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false,
+ 4, 1));
+ mCreateActivityToggle.setText("Create activity");
+ mCreateActivityToggle.setSelection(mValues.createActivity);
+ mCreateActivityToggle.addSelectionListener(this);
+
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ mLibraryToggle = new Button(container, SWT.CHECK);
+ mLibraryToggle.setSelection(true);
+ mLibraryToggle.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mLibraryToggle.setText("Mark this project as a library");
+ mLibraryToggle.setSelection(mValues.isLibrary);
+ mLibraryToggle.addSelectionListener(this);
+
+ // Blank line
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ mUseDefaultLocationToggle = new Button(container, SWT.CHECK);
+ mUseDefaultLocationToggle.setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+ mUseDefaultLocationToggle.setText("Create Project in Workspace");
+ mUseDefaultLocationToggle.addSelectionListener(this);
+
+ mLocationLabel = new Label(container, SWT.NONE);
+ mLocationLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ mLocationLabel.setText("Location:");
+
+ mLocationText = new Text(container, SWT.BORDER);
+ mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+ mLocationText.addModifyListener(this);
+
+ mChooseLocationButton = new Button(container, SWT.NONE);
+ mChooseLocationButton.setText("Browse...");
+ mChooseLocationButton.addSelectionListener(this);
+ mChooseLocationButton.setEnabled(false);
+ setUseCustomLocation(!mValues.useDefaultLocation);
+
+ new Label(container, SWT.NONE).setLayoutData(
+ new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
+
+ Composite group = mWorkingSetGroup.createControl(container);
+ group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 4, 1));
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+
+ if (visible) {
+ try {
+ mIgnore = true;
+ mUseDefaultLocationToggle.setSelection(mValues.useDefaultLocation);
+ mLocationText.setText(mValues.projectLocation);
+ } finally {
+ mIgnore = false;
+ }
+ }
+
+ validatePage();
+ }
+
+ private void setUseCustomLocation(boolean en) {
+ mValues.useDefaultLocation = !en;
+ mUseDefaultLocationToggle.setSelection(!en);
+ if (!en) {
+ updateProjectLocation(mValues.projectName);
+ }
+
+ mLocationLabel.setEnabled(en);
+ mLocationText.setEnabled(en);
+ mChooseLocationButton.setEnabled(en);
+ }
+
+ void init(IStructuredSelection selection, IWorkbenchPart activePart) {
+ setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart));
+ }
+
+ /**
+ * Returns the working sets to which the new project should be added.
+ *
+ * @return the selected working sets to which the new project should be added
+ */
+ private IWorkingSet[] getWorkingSets() {
+ return mWorkingSetGroup.getSelectedWorkingSets();
+ }
+
+ /**
+ * Sets the working sets to which the new project should be added.
+ *
+ * @param workingSets the initial selected working sets
+ */
+ private void setWorkingSets(IWorkingSet[] workingSets) {
+ assert workingSets != null;
+ mWorkingSetGroup.setWorkingSets(workingSets);
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ // Sync working set data to the value object, since the WorkingSetGroup
+ // doesn't let us add listeners to do this lazily
+ mValues.workingSets = getWorkingSets();
+
+ return super.getNextPage();
+ }
+
+ // ---- Implements ModifyListener ----
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mLocationText) {
+ mValues.projectLocation = mLocationText.getText().trim();
+ }
+
+ validatePage();
+ }
+
+
+ /** If the project should be created in the workspace, then update the project location
+ * based on the project name. */
+ private void updateProjectLocation(String projectName) {
+ if (projectName == null) {
+ projectName = "";
+ }
+
+ boolean useDefaultLocation = mUseDefaultLocationToggle.getSelection();
+
+ if (useDefaultLocation) {
+ IPath workspace = Platform.getLocation();
+ String projectLocation = workspace.append(projectName).toOSString();
+ mLocationText.setText(projectLocation);
+ mValues.projectLocation = projectLocation;
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mIgnore) {
+ return;
+ }
+
+ Object source = e.getSource();
+ if (source == mCustomIconToggle) {
+ mValues.createIcon = mCustomIconToggle.getSelection();
+ } else if (source == mLibraryToggle) {
+ mValues.isLibrary = mLibraryToggle.getSelection();
+ } else if (source == mCreateActivityToggle) {
+ mValues.createActivity = mCreateActivityToggle.getSelection();
+ } else if (source == mUseDefaultLocationToggle) {
+ boolean useDefault = mUseDefaultLocationToggle.getSelection();
+ setUseCustomLocation(!useDefault);
+ } else if (source == mChooseLocationButton) {
+ String dir = promptUserForLocation(getShell());
+ if (dir != null) {
+ mLocationText.setText(dir);
+ mValues.projectLocation = dir;
+ }
+ }
+
+ validatePage();
+ }
+
+ private String promptUserForLocation(Shell shell) {
+ DirectoryDialog dd = new DirectoryDialog(getShell());
+ dd.setMessage("Select folder where project should be created");
+
+ String curLocation = mLocationText.getText().trim();
+ if (!curLocation.isEmpty()) {
+ dd.setFilterPath(curLocation);
+ } else if (sLastProjectLocation != null) {
+ dd.setFilterPath(sLastProjectLocation);
+ }
+
+ String dir = dd.open();
+ if (dir != null) {
+ sLastProjectLocation = dir;
+ }
+
+ return dir;
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+
+ // ---- Implements FocusListener ----
+
+ @Override
+ public void focusGained(FocusEvent e) {
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ }
+
+ // Validation
+
+ void validatePage() {
+ IStatus status = validateProjectLocation();
+
+ 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);
+ }
+ }
+
+ static IStatus validateLocationInWorkspace(NewProjectWizardState values) {
+ if (values.useDefaultLocation) {
+ return null;
+ }
+
+ // Validate location
+ if (values.projectName != null) {
+ File dest = Platform.getLocation().append(values.projectName).toFile();
+ if (dest.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
+ "There is already a file or directory named \"%1$s\" in the selected location.",
+ values.projectName));
+ }
+ }
+
+ return null;
+ }
+
+ private IStatus validateProjectLocation() {
+ if (mValues.useDefaultLocation) {
+ return validateLocationInWorkspace(mValues);
+ }
+
+ String location = mLocationText.getText();
+ if (location.trim().isEmpty()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Provide a valid file system location where the project should be created.");
+ }
+
+ File f = new File(location);
+ if (f.exists()) {
+ if (!f.isDirectory()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not a valid folder.", location));
+ }
+
+ File[] children = f.listFiles();
+ if (children != null && children.length > 0) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Folder '%s' is not empty.", location));
+ }
+ }
+
+ // if the folder doesn't exist, then make sure that the parent
+ // exists and is a writable folder
+ File parent = f.getParentFile();
+ if (!parent.exists()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("Folder '%s' does not exist.", parent.getName()));
+ }
+
+ if (!parent.isDirectory()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not a folder.", parent.getName()));
+ }
+
+ if (!parent.canWrite()) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("'%s' is not writeable.", parent.getName()));
+ }
+
+ return null;
+ }
+}
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 000000000..c1c8073c0
--- /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 000000000..8e11841b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java
@@ -0,0 +1,1239 @@
+/*
+ * 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.SdkConstants.ATTR_PACKAGE;
+import static com.android.SdkConstants.DOT_AIDL;
+import static com.android.SdkConstants.DOT_FTL;
+import static com.android.SdkConstants.DOT_JAVA;
+import static com.android.SdkConstants.DOT_RS;
+import static com.android.SdkConstants.DOT_SVG;
+import static com.android.SdkConstants.DOT_TXT;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_NATIVE_LIBS;
+import static com.android.SdkConstants.XMLNS_PREFIX;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.xml.XmlFormatStyle;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
+import com.android.manifmerger.ManifestMerger;
+import com.android.manifmerger.MergerLog;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.SdkUtils;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+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.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.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.ToolFactory;
+import org.eclipse.jdt.core.formatter.CodeFormatter;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.swt.SWT;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.Attributes;
+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.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Handler which manages instantiating FreeMarker templates, copying resources
+ * and merging into existing files
+ */
+class TemplateHandler {
+ /** Highest supported format; templates with a higher number will be skipped
+ * <p>
+ * <ul>
+ * <li> 1: Initial format, supported by ADT 20 and up.
+ * <li> 2: ADT 21 and up. Boolean variables that have a default value and are not
+ * edited by the user would end up as strings in ADT 20; now they are always
+ * proper Booleans. Templates which rely on this should specify format >= 2.
+ * <li> 3: The wizard infrastructure passes the {@code isNewProject} boolean variable
+ * to indicate whether a wizard is created as part of a new blank project
+ * <li> 4: The templates now specify dependencies in the recipe file.
+ * </ul>
+ */
+ static final int CURRENT_FORMAT = 4;
+
+ /**
+ * 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 = "resources"; //$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 TAG_DEPENDENCY = "dependency"; //$NON-NLS-1$
+ static final String TAG_ICONS = "icons"; //$NON-NLS-1$
+ static final String TAG_FORMFACTOR = "formfactor"; //$NON-NLS-1$
+ static final String TAG_CATEGORY = "category"; //$NON-NLS-1$
+ static final String ATTR_FORMAT = "format"; //$NON-NLS-1$
+ static final String ATTR_REVISION = "revision"; //$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$
+ static final String ATTR_BACKGROUND = "background"; //$NON-NLS-1$
+ static final String ATTR_FOREGROUND = "foreground"; //$NON-NLS-1$
+ static final String ATTR_SHAPE = "shape"; //$NON-NLS-1$
+ static final String ATTR_TRIM = "trim"; //$NON-NLS-1$
+ static final String ATTR_PADDING = "padding"; //$NON-NLS-1$
+ static final String ATTR_SOURCE_TYPE = "source"; //$NON-NLS-1$
+ static final String ATTR_CLIPART_NAME = "clipartName";//$NON-NLS-1$
+ static final String ATTR_TEXT = "text"; //$NON-NLS-1$
+ static final String ATTR_SRC_DIR = "srcDir"; //$NON-NLS-1$
+ static final String ATTR_SRC_OUT = "srcOut"; //$NON-NLS-1$
+ static final String ATTR_RES_DIR = "resDir"; //$NON-NLS-1$
+ static final String ATTR_RES_OUT = "resOut"; //$NON-NLS-1$
+ static final String ATTR_MANIFEST_DIR = "manifestDir";//$NON-NLS-1$
+ static final String ATTR_MANIFEST_OUT = "manifestOut";//$NON-NLS-1$
+ static final String ATTR_PROJECT_DIR = "projectDir"; //$NON-NLS-1$
+ static final String ATTR_PROJECT_OUT = "projectOut"; //$NON-NLS-1$
+ static final String ATTR_MAVEN_URL = "mavenUrl"; //$NON-NLS-1$
+ static final String ATTR_DEBUG_KEYSTORE_SHA1 =
+ "debugKeystoreSha1"; //$NON-NLS-1$
+
+ static final String CATEGORY_ACTIVITIES = "activities";//$NON-NLS-1$
+ static final String CATEGORY_PROJECTS = "projects"; //$NON-NLS-1$
+ static final String CATEGORY_OTHER = "other"; //$NON-NLS-1$
+
+ static final String MAVEN_SUPPORT_V4 = "support-v4"; //$NON-NLS-1$
+ static final String MAVEN_SUPPORT_V13 = "support-v13"; //$NON-NLS-1$
+ static final String MAVEN_APPCOMPAT = "appcompat-v7"; //$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();
+
+ /**
+ * List of actions to perform after the wizard has finished.
+ */
+ protected List<Runnable> mFinalizingActions = Lists.newArrayList();
+
+ /** Path to the directory containing the templates */
+ @NonNull
+ private final File mRootPath;
+
+ /** The changes being processed by the template handler */
+ private List<Change> mMergeChanges;
+ private List<Change> mTextChanges;
+ private List<Change> mOtherChanges;
+
+ /** The project to write the template into */
+ private IProject mProject;
+
+ /** 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;
+
+ private final TemplateManager mManager;
+
+ /** Creates a new {@link TemplateHandler} for the given root path */
+ static TemplateHandler createFromPath(File rootPath) {
+ return new TemplateHandler(rootPath, new TemplateManager());
+ }
+
+ /** Creates a new {@link TemplateHandler} for the template name, which should
+ * be relative to the templates directory */
+ static TemplateHandler createFromName(String category, String name) {
+ TemplateManager manager = new TemplateManager();
+
+ // Use the TemplateManager iteration which should merge contents between the
+ // extras/templates/ and tools/templates folders and pick the most recent version
+ List<File> templates = manager.getTemplates(category);
+ for (File file : templates) {
+ if (file.getName().equals(name) && category.equals(file.getParentFile().getName())) {
+ return new TemplateHandler(file, manager);
+ }
+ }
+
+ return new TemplateHandler(new File(getTemplateRootFolder(),
+ category + File.separator + name), manager);
+ }
+
+ private TemplateHandler(File rootPath, TemplateManager manager) {
+ mRootPath = rootPath;
+ mManager = manager;
+ mLoader = new MyTemplateLoader();
+ mLoader.setPrefix(mRootPath.getPath());
+ }
+
+ public TemplateManager getManager() {
+ return mManager;
+ }
+
+ public void setBackupMergedFiles(boolean backupMergedFiles) {
+ mBackupMergedFiles = backupMergedFiles;
+ }
+
+ @NonNull
+ public List<Change> render(IProject project, Map<String, Object> args) {
+ mOpen.clear();
+
+ mProject = project;
+ mMergeChanges = new ArrayList<Change>();
+ mTextChanges = new ArrayList<Change>();
+ mOtherChanges = new ArrayList<Change>();
+
+ // 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);
+
+ // Add the changes in the order where merges are shown first, then text files,
+ // and finally other files (like jars and icons which don't have previews).
+ List<Change> changes = new ArrayList<Change>();
+ changes.addAll(mMergeChanges);
+ changes.addAll(mTextChanges);
+ changes.addAll(mOtherChanges);
+ return changes;
+ }
+
+ 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$
+ paramMap.put("classToResource", new FmClassNameToResourceMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlAttribute", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlText", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("escapeXmlString", new FmEscapeXmlStringMethod()); //$NON-NLS-1$
+ paramMap.put("extractLetters", new FmExtractLettersMethod()); //$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;
+ }
+
+ static void addDirectoryParameters(Map<String, Object> parameters, IProject project) {
+ IPath srcDir = project.getFile(SdkConstants.SRC_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_SRC_DIR, srcDir.toString());
+
+ IPath resDir = project.getFile(SdkConstants.RES_FOLDER).getProjectRelativePath();
+ parameters.put(ATTR_RES_DIR, resDir.toString());
+
+ IPath manifestDir = project.getProjectRelativePath();
+ parameters.put(ATTR_MANIFEST_DIR, manifestDir.toString());
+ parameters.put(ATTR_MANIFEST_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_PROJECT_DIR, manifestDir.toString());
+ parameters.put(ATTR_PROJECT_OUT, manifestDir.toString());
+
+ parameters.put(ATTR_DEBUG_KEYSTORE_SHA1, "");
+ }
+
+ @Nullable
+ public TemplateMetadata getTemplate() {
+ if (mTemplate == null) {
+ mTemplate = mManager.getTemplate(mRootPath);
+ }
+
+ return mTemplate;
+ }
+
+ @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) {
+ try {
+ return Files.toString(new File(mRootPath,
+ relativePath.replace('/', File.separatorChar)), Charsets.UTF_8);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ @Nullable
+ public String readTemplateTextResource(@NonNull File file) {
+ assert file.isAbsolute();
+ 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) {
+ try {
+ return Files.toByteArray(new File(mRootPath, relativePath));
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ return null;
+ }
+ }
+
+ /**
+ * Most recent thrown exception during template instantiation. This should
+ * basically always be null. Used by unit tests to see if any template
+ * instantiation recorded a failure.
+ */
+ @VisibleForTesting
+ public static Exception sMostRecentException;
+
+ /** Read the given FreeMarker file and process the variable definitions */
+ private void processVariables(final Configuration freemarker,
+ String file, final Map<String, Object> paramMap) {
+ try {
+ String xml;
+ if (file.endsWith(DOT_XML)) {
+ // Just read the file
+ xml = readTemplateTextResource(file);
+ if (xml == null) {
+ return;
+ }
+ } 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);
+ Object mapValue = value;
+ if (value != null && !value.isEmpty()) {
+ String type = attributes.getValue(ATTR_TYPE);
+ if ("boolean".equals(type)) { //$NON-NLS-1$
+ mapValue = Boolean.valueOf(value);
+ }
+ }
+ paramMap.put(id, mapValue);
+ }
+ } else if (TAG_GLOBAL.equals(name)) {
+ String id = attributes.getValue(ATTR_ID);
+ if (!paramMap.containsKey(id)) {
+ paramMap.put(id, TypedVariable.parseGlobal(attributes));
+ }
+ } else if (TAG_GLOBALS.equals(name)) {
+ // Handle evaluation of variables
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ processVariables(freemarker, path, paramMap);
+ } // else: <globals> root element
+ } else if (TAG_EXECUTE.equals(name)) {
+ String path = attributes.getValue(ATTR_FILE);
+ if (path != null) {
+ execute(freemarker, path, paramMap);
+ }
+ } else if (TAG_DEPENDENCY.equals(name)) {
+ String dependencyName = attributes.getValue(ATTR_NAME);
+ if (dependencyName.equals(SUPPORT_LIBRARY_NAME)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ File path = AddSupportJarAction.getSupportJarFile();
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ }
+ } else if (!name.equals("template") && !name.equals(TAG_CATEGORY) &&
+ !name.equals(TAG_FORMFACTOR) && !name.equals("option") &&
+ !name.equals(TAG_THUMBS) && !name.equals(TAG_THUMB) &&
+ !name.equals(TAG_ICONS)) {
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ }
+ });
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private boolean canOverwrite(File file) {
+ if (file.exists()) {
+ // 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) {
+ 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);
+ }
+ IPath to = getTargetPath(toPath);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, fromPath, to);
+ } else {
+ copyTemplateResource(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/
+ IPath to = getTargetPath(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 (TAG_DEPENDENCY.equals(name)) {
+ String dependencyUrl = attributes.getValue(ATTR_MAVEN_URL);
+ File path;
+ if (dependencyUrl.contains(MAVEN_SUPPORT_V4)) {
+ // We assume the revision requirement has been satisfied
+ // by the wizard
+ path = AddSupportJarAction.getSupportJarFile();
+ } else if (dependencyUrl.contains(MAVEN_SUPPORT_V13)) {
+ path = AddSupportJarAction.getSupport13JarFile();
+ } else if (dependencyUrl.contains(MAVEN_APPCOMPAT)) {
+ path = null;
+ mFinalizingActions.add(new Runnable() {
+ @Override
+ public void run() {
+ AddSupportJarAction.installAppCompatLibrary(mProject, true);
+ }
+ });
+ } else {
+ path = null;
+ System.err.println("WARNING: Unknown dependency type");
+ }
+
+ if (path != null) {
+ IPath to = getTargetPath(FD_NATIVE_LIBS +'/' + path.getName());
+ try {
+ copy(path, to);
+ } catch (IOException ioe) {
+ AdtPlugin.log(ioe, null);
+ }
+ }
+ } else if (!name.equals("recipe") && !name.equals(TAG_DEPENDENCY)) { //$NON-NLS-1$
+ System.err.println("WARNING: Unknown template directive " + name);
+ }
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+ });
+
+ } catch (Exception e) {
+ sMostRecentException = e;
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ @NonNull
+ private File getFullPath(@NonNull String fromPath) {
+ if (fromPath.startsWith(VALUE_TEMPLATE_DIR)) {
+ return new File(getTemplateRootFolder(), RESOURCE_ROOT + File.separator
+ + fromPath.substring(VALUE_TEMPLATE_DIR.length() + 1).replace('/',
+ File.separatorChar));
+ }
+ return new File(mRootPath, DATA_ROOT + File.separator + fromPath);
+ }
+
+ @NonNull
+ private IPath getTargetPath(@NonNull String relative) {
+ if (relative.indexOf('\\') != -1) {
+ relative = relative.replace('\\', '/');
+ }
+ return new Path(relative);
+ }
+
+ @NonNull
+ private IFile getTargetFile(@NonNull IPath path) {
+ return mProject.getFile(path);
+ }
+
+ private void merge(
+ @NonNull final Configuration freemarker,
+ @NonNull final Map<String, Object> paramMap,
+ @NonNull String relativeFrom,
+ @NonNull IPath toPath) throws IOException, TemplateException {
+
+ String currentXml = null;
+
+ IFile to = getTargetFile(toPath);
+ if (to.exists()) {
+ currentXml = AdtPlugin.readFile(to);
+ }
+
+ if (currentXml == null) {
+ // The target file doesn't exist: don't merge, just copy
+ boolean instantiate = relativeFrom.endsWith(DOT_FTL);
+ if (instantiate) {
+ instantiate(freemarker, paramMap, relativeFrom, toPath);
+ } else {
+ copyTemplateResource(relativeFrom, toPath);
+ }
+ return;
+ }
+
+ if (!to.getFileExtension().equals(EXT_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);
+ if (xml == null) {
+ return;
+ }
+ }
+
+ Document currentDocument = DomUtilities.parseStructuredDocument(currentXml);
+ assert currentDocument != null : currentXml;
+ Document fragment = DomUtilities.parseStructuredDocument(xml);
+ assert fragment != null : xml;
+
+ XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST;
+ boolean modified;
+ boolean ok;
+ String fileName = to.getName();
+ if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
+ modified = ok = mergeManifest(currentDocument, fragment);
+ } else {
+ // Merge plain XML files
+ String parentFolderName = to.getParent().getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName);
+ if (folderType != null) {
+ formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath);
+ } else {
+ formatStyle = XmlFormatStyle.FILE;
+ }
+
+ modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap);
+ ok = true;
+ }
+
+ // Finally write out the merged file (formatting etc)
+ String contents = null;
+ if (ok) {
+ if (modified) {
+ contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument,
+ EclipseXmlFormatPreferences.create(), formatStyle, null,
+ currentXml.endsWith("\n")); //$NON-NLS-1$
+ }
+ } else {
+ // Just insert into file along with comment, using the "standard" conflict
+ // syntax that many tools and editors recognize.
+ String sep = SdkUtils.getLineSeparator();
+ contents =
+ "<<<<<<< Original" + sep
+ + currentXml + sep
+ + "=======" + sep
+ + xml
+ + ">>>>>>> Added" + sep;
+ }
+
+ if (contents != null) {
+ TextFileChange change = new TextFileChange("Merge " + fileName, to);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new ReplaceEdit(0, currentXml.length(), contents));
+ change.setEdit(rootEdit);
+ change.setTextType(SdkConstants.EXT_XML);
+ mMergeChanges.add(change);
+ }
+ }
+
+ /** Merges the given resource file contents into the given resource file
+ * @param paramMap */
+ private static boolean mergeResourceFile(Document currentDocument, Document fragment,
+ ResourceFolderType folderType, Map<String, Object> paramMap) {
+ boolean modified = false;
+
+ // Copy namespace declarations
+ NamedNodeMap attributes = fragment.getDocumentElement().getAttributes();
+ if (attributes != null) {
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ if (attribute.getName().startsWith(XMLNS_PREFIX)) {
+ currentDocument.getDocumentElement().setAttribute(attribute.getName(),
+ attribute.getValue());
+ }
+ }
+ }
+
+ // For layouts for example, I want to *append* inside the root all the
+ // contents of the new file.
+ // But for resources for example, I want to combine elements which specify
+ // 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);
+ }
+ Collections.reverse(nodes);
+
+ root = currentDocument.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 static boolean mergeManifest(Document currentManifest, Document fragment) {
+ // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create
+ // and maintain error markers.
+
+ // Transfer package element from manifest to merged in root; required by
+ // manifest merger
+ Element fragmentRoot = fragment.getDocumentElement();
+ Element manifestRoot = currentManifest.getDocumentElement();
+ if (fragmentRoot == null || manifestRoot == null) {
+ return false;
+ }
+ String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg == null || pkg.isEmpty()) {
+ pkg = manifestRoot.getAttribute(ATTR_PACKAGE);
+ if (pkg != null && !pkg.isEmpty()) {
+ fragmentRoot.setAttribute(ATTR_PACKAGE, pkg);
+ }
+ }
+
+ ManifestMerger merger = new ManifestMerger(
+ MergerLog.wrapSdkLog(AdtPlugin.getDefault()),
+ new AdtManifestMergeCallback()).setExtractPackagePrefix(true);
+ 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 IPath to) throws IOException, TemplateException {
+ // 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
+ copyTemplateResource(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();
+
+ contents = format(mProject, contents, to);
+ IFile targetFile = getTargetFile(to);
+ TextFileChange change = createNewFileChange(targetFile);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChild(new InsertEdit(0, contents));
+ change.setEdit(rootEdit);
+ mTextChanges.add(change);
+ }
+ }
+
+ private static String format(IProject project, String contents, IPath to) {
+ String name = to.lastSegment();
+ if (name.endsWith(DOT_XML)) {
+ XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to);
+ EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create();
+ return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null);
+ } else if (name.endsWith(DOT_JAVA)) {
+ Map<?, ?> options = null;
+ if (project != null && project.isAccessible()) {
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject != null) {
+ options = javaProject.getOptions(true);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ if (options == null) {
+ options = JavaCore.getOptions();
+ }
+
+ CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
+
+ try {
+ IDocument doc = new org.eclipse.jface.text.Document();
+ // format the file (the meat and potatoes)
+ doc.set(contents);
+ TextEdit edit = formatter.format(
+ CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS,
+ contents, 0, contents.length(), 0, null);
+ if (edit != null) {
+ edit.apply(doc);
+ }
+
+ return doc.get();
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return contents;
+ }
+
+ private static TextFileChange createNewFileChange(IFile targetFile) {
+ String fileName = targetFile.getName();
+ String message;
+ if (targetFile.exists()) {
+ message = String.format("Replace %1$s", fileName);
+ } else {
+ message = String.format("Create %1$s", fileName);
+ }
+
+ TextFileChange change = new TextFileChange(message, targetFile) {
+ @Override
+ protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException {
+ IDocument document = super.acquireDocument(pm);
+
+ // In our case, we know we *always* use this TextFileChange
+ // to *create* files, we're not appending to existing files.
+ // However, due to the following bug we can end up with cached
+ // contents of previously deleted files that happened to have the
+ // same file name:
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402
+ // Therefore, as a workaround, wipe out the cached contents here
+ if (document.getLength() > 0) {
+ try {
+ document.replace(0, document.getLength(), "");
+ } catch (BadLocationException e) {
+ // pass
+ }
+ }
+
+ return document;
+ }
+ };
+ change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1));
+ return change;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Returns the list of actions to perform when the template has been created
+ *
+ * @return the list of actions to perform
+ */
+ @NonNull
+ public List<Runnable> getFinalizingActions() {
+ return mFinalizingActions;
+ }
+
+ /** Copy a template resource */
+ private final void copyTemplateResource(
+ @NonNull String relativeFrom,
+ @NonNull IPath output) throws IOException {
+ File from = getFullPath(relativeFrom);
+ copy(from, output);
+ }
+
+ /** Returns true if the given file contains the given bytes */
+ private static boolean isIdentical(@Nullable byte[] data, @NonNull IFile dest) {
+ assert dest.exists();
+ byte[] existing = AdtUtils.readData(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, IPath path) throws IOException {
+ if (src.isDirectory()) {
+ File[] children = src.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ copy(child, path.append(child.getName()));
+ }
+ }
+ } else {
+ IResource dest = mProject.getFile(path);
+ if (dest.exists() && !(dest instanceof IFile)) {// Don't attempt to overwrite a folder
+ assert false : dest.getClass().getName();
+ return;
+ }
+ IFile file = (IFile) dest;
+ String targetName = path.lastSegment();
+ if (dest instanceof IFile) {
+ if (dest.exists() && isIdentical(Files.toByteArray(src), file)) {
+ String label = String.format(
+ "Not overwriting %1$s because the files are identical", targetName);
+ NullChange change = new NullChange(label);
+ change.setEnabled(false);
+ mOtherChanges.add(change);
+ return;
+ }
+ }
+
+ if (targetName.endsWith(DOT_XML)
+ || targetName.endsWith(DOT_JAVA)
+ || targetName.endsWith(DOT_TXT)
+ || targetName.endsWith(DOT_RS)
+ || targetName.endsWith(DOT_AIDL)
+ || targetName.endsWith(DOT_SVG)) {
+
+ String newFile = Files.toString(src, Charsets.UTF_8);
+ newFile = format(mProject, newFile, path);
+
+ TextFileChange addFile = createNewFileChange(file);
+ addFile.setEdit(new InsertEdit(0, newFile));
+ mTextChanges.add(addFile);
+ } else {
+ // Write binary file: Need custom change for that
+ IPath workspacePath = mProject.getFullPath().append(path);
+ mOtherChanges.add(new CreateFileChange(targetName, workspacePath, src));
+ }
+ }
+ }
+
+ /**
+ * 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;
+ File file = new File(path);
+ if (file.exists()) {
+ return file.toURI().toURL();
+ }
+ return null;
+ }
+
+ @Override
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+ }
+
+ /**
+ * Validates this template to make sure it's supported
+ * @param currentMinSdk the minimum SDK in the project, or -1 or 0 if unknown (e.g. codename)
+ * @param buildApi the build API, or -1 or 0 if unknown (e.g. codename)
+ *
+ * @return a status object with the error, or null if there is no problem
+ */
+ @SuppressWarnings("cast") // In Eclipse 3.6.2 cast below is needed
+ @Nullable
+ public IStatus validateTemplate(int currentMinSdk, int buildApi) {
+ TemplateMetadata template = getTemplate();
+ if (template == null) {
+ return null;
+ }
+ if (!template.isSupported()) {
+ String versionString = (String) AdtPlugin.getDefault().getBundle().getHeaders().get(
+ Constants.BUNDLE_VERSION);
+ Version version = new Version(versionString);
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a more recent version of the " +
+ "Android Eclipse plugin. Please update from version %1$d.%2$d.%3$d.",
+ version.getMajor(), version.getMinor(), version.getMicro()));
+ }
+ int templateMinSdk = template.getMinSdk();
+ if (templateMinSdk > currentMinSdk && currentMinSdk >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a minimum SDK version of at " +
+ "least %1$d, and the current min version is %2$d",
+ templateMinSdk, currentMinSdk));
+ }
+ int templateMinBuildApi = template.getMinBuildApi();
+ if (templateMinBuildApi > buildApi && buildApi >= 1) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("This template requires a build target API version of at " +
+ "least %1$d, and the current version is %2$d",
+ templateMinBuildApi, buildApi));
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java
new file mode 100644
index 000000000..30dd09e31
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateManager.java
@@ -0,0 +1,261 @@
+/*
+ * 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.SdkConstants.FD_EXTRAS;
+import static com.android.SdkConstants.FD_TEMPLATES;
+import static com.android.SdkConstants.FD_TOOLS;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TEMPLATE_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.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+
+import org.w3c.dom.Document;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Handles locating templates and providing template metadata */
+public class TemplateManager {
+ private static final Set<String> EXCLUDED_CATEGORIES = Sets.newHashSet("Folder", "Google");
+ private static final Set<String> EXCLUDED_FORMFACTORS = Sets.newHashSet("Wear", "TV");
+
+ TemplateManager() {
+ }
+
+ /** @return the root folder containing templates */
+ @Nullable
+ public static File getTemplateRootFolder() {
+ String location = AdtPrefs.getPrefs().getOsSdkFolder();
+ if (location != null) {
+ File folder = new File(location, FD_TOOLS + File.separator + FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ return folder;
+ }
+ }
+
+ return null;
+ }
+
+ /** @return the root folder containing extra templates */
+ @NonNull
+ public static List<File> getExtraTemplateRootFolders() {
+ List<File> folders = new ArrayList<File>();
+ String location = AdtPrefs.getPrefs().getOsSdkFolder();
+ if (location != null) {
+ File extras = new File(location, FD_EXTRAS);
+ if (extras.isDirectory()) {
+ for (File vendor : AdtUtils.listFiles(extras)) {
+ if (!vendor.isDirectory()) {
+ continue;
+ }
+ for (File pkg : AdtUtils.listFiles(vendor)) {
+ if (pkg.isDirectory()) {
+ File folder = new File(pkg, FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ folders.add(folder);
+ }
+ }
+ }
+ }
+
+ // Legacy
+ File folder = new File(extras, FD_TEMPLATES);
+ if (folder.isDirectory()) {
+ folders.add(folder);
+ }
+ }
+ }
+
+ return folders;
+ }
+
+ /**
+ * Returns a template file under the given root, if it exists
+ *
+ * @param root the root folder
+ * @param relativePath the relative path
+ * @return a template file under the given root, if it exists
+ */
+ @Nullable
+ public static File getTemplateLocation(@NonNull File root, @NonNull String relativePath) {
+ File templateRoot = getTemplateRootFolder();
+ if (templateRoot != null) {
+ String rootPath = root.getPath();
+ File templateFile = new File(templateRoot,
+ rootPath.replace('/', File.separatorChar) + File.separator
+ + relativePath.replace('/', File.separatorChar));
+ if (templateFile.exists()) {
+ return templateFile;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a template file under one of the available roots, if it exists
+ *
+ * @param relativePath the relative path
+ * @return a template file under one of the available roots, if it exists
+ */
+ @Nullable
+ public static File getTemplateLocation(@NonNull String relativePath) {
+ File templateRoot = getTemplateRootFolder();
+ if (templateRoot != null) {
+ File templateFile = new File(templateRoot,
+ relativePath.replace('/', File.separatorChar));
+ if (templateFile.exists()) {
+ return templateFile;
+ }
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Returns all the templates with the given prefix
+ *
+ * @param folder the folder prefix
+ * @return the available templates
+ */
+ @NonNull
+ List<File> getTemplates(@NonNull String folder) {
+ List<File> templates = new ArrayList<File>();
+ Map<String, File> templateNames = Maps.newHashMap();
+ File root = getTemplateRootFolder();
+ if (root != null) {
+ File[] files = new File(root, folder).listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) { // Avoid .DS_Store etc
+ templates.add(file);
+ templateNames.put(file.getName(), file);
+ }
+ }
+ }
+ }
+
+ // Add in templates from extras/ as well.
+ for (File extra : getExtraTemplateRootFolders()) {
+ File[] files = new File(extra, folder).listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ File replaces = templateNames.get(file.getName());
+ if (replaces != null) {
+ int compare = compareTemplates(replaces, file);
+ if (compare > 0) {
+ int index = templates.indexOf(replaces);
+ if (index != -1) {
+ templates.set(index, file);
+ } else {
+ templates.add(file);
+ }
+ }
+ } else {
+ templates.add(file);
+ }
+ }
+ }
+ }
+ }
+
+ // Sort by file name (not path as is File's default)
+ if (templates.size() > 1) {
+ Collections.sort(templates, new Comparator<File>() {
+ @Override
+ public int compare(File file1, File file2) {
+ return file1.getName().compareTo(file2.getName());
+ }
+ });
+ }
+
+ return templates;
+ }
+
+ /**
+ * Compare two files, and return the one with the HIGHEST revision, and if
+ * the same, most recently modified
+ */
+ private int compareTemplates(File file1, File file2) {
+ TemplateMetadata template1 = getTemplate(file1);
+ TemplateMetadata template2 = getTemplate(file2);
+
+ if (template1 == null) {
+ return 1;
+ } else if (template2 == null) {
+ return -1;
+ } else {
+ int delta = template2.getRevision() - template1.getRevision();
+ if (delta == 0) {
+ delta = (int) (file2.lastModified() - file1.lastModified());
+ }
+ return delta;
+ }
+ }
+
+ /** Cache for {@link #getTemplate()} */
+ private Map<File, TemplateMetadata> mTemplateMap;
+
+ @Nullable
+ TemplateMetadata getTemplate(File templateDir) {
+ if (mTemplateMap != null) {
+ TemplateMetadata metadata = mTemplateMap.get(templateDir);
+ if (metadata != null) {
+ return metadata;
+ }
+ } else {
+ mTemplateMap = Maps.newHashMap();
+ }
+
+ try {
+ File templateFile = new File(templateDir, TEMPLATE_XML);
+ if (templateFile.isFile()) {
+ String xml = Files.toString(templateFile, Charsets.UTF_8);
+ Document doc = DomUtilities.parseDocument(xml, true);
+ if (doc != null && doc.getDocumentElement() != null) {
+ TemplateMetadata metadata = new TemplateMetadata(doc);
+ if (EXCLUDED_CATEGORIES.contains(metadata.getCategory()) ||
+ EXCLUDED_FORMFACTORS.contains(metadata.getFormFactor())) {
+ return null;
+ }
+ mTemplateMap.put(templateDir, metadata);
+ return metadata;
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/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 000000000..4ce7d74c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java
@@ -0,0 +1,468 @@
+/*
+ * 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_BUILD_API;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_REVISION;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_BACKGROUND;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CLIPART_NAME;
+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_FOREGROUND;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_FORMAT;
+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_PADDING;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SHAPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_SOURCE_TYPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TEXT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TRIM;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_TYPE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_VALUE;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.CURRENT_FORMAT;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_DEPENDENCY;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_ICONS;
+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 static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_FORMFACTOR;
+import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.TAG_CATEGORY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.assetstudiolib.GraphicGenerator;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.assetstudio.AssetType;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState;
+import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState.SourceType;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils;
+import com.android.utils.Pair;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.RGB;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+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;
+ private List<Pair<String, Integer>> mDependencies;
+ private Integer mMinApi;
+ private Integer mMinBuildApi;
+ private Integer mRevision;
+ private boolean mNoIcons;
+ private CreateAssetSetWizardState mIconState;
+ private String mFormFactor;
+ private String mCategory;
+
+ 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(this, element);
+ mParameters.add(parameter);
+ if (parameter.id != null) {
+ mParameterMap.put(parameter.id, parameter);
+ }
+ }
+ }
+
+ boolean isSupported() {
+ String versionString = mDocument.getDocumentElement().getAttribute(ATTR_FORMAT);
+ if (versionString != null && !versionString.isEmpty()) {
+ try {
+ int version = Integer.parseInt(versionString);
+ return version <= CURRENT_FORMAT;
+ } catch (NumberFormatException nufe) {
+ return false;
+ }
+ }
+
+ // Older templates without version specified: supported
+ return true;
+ }
+
+ @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;
+ }
+
+ int getMinSdk() {
+ if (mMinApi == null) {
+ mMinApi = 1;
+ String api = mDocument.getDocumentElement().getAttribute(ATTR_MIN_API);
+ if (api != null && !api.isEmpty()) {
+ try {
+ mMinApi = Integer.parseInt(api);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should always be an integer
+ AdtPlugin.log(nufe, null);
+ mMinApi = 1;
+ }
+ }
+ }
+
+ return mMinApi.intValue();
+ }
+
+ int getMinBuildApi() {
+ if (mMinBuildApi == null) {
+ mMinBuildApi = 1;
+ String api = mDocument.getDocumentElement().getAttribute(ATTR_MIN_BUILD_API);
+ if (api != null && !api.isEmpty()) {
+ try {
+ mMinBuildApi = Integer.parseInt(api);
+ } catch (NumberFormatException nufe) {
+ // Templates aren't allowed to contain codenames, should always be an integer
+ AdtPlugin.log(nufe, null);
+ mMinBuildApi = 1;
+ }
+ }
+ }
+
+ return mMinBuildApi.intValue();
+ }
+
+ public int getRevision() {
+ if (mRevision == null) {
+ mRevision = 1;
+ String revision = mDocument.getDocumentElement().getAttribute(ATTR_REVISION);
+ if (revision != null && !revision.isEmpty()) {
+ try {
+ mRevision = Integer.parseInt(revision);
+ } catch (NumberFormatException nufe) {
+ AdtPlugin.log(nufe, null);
+ mRevision = 1;
+ }
+ }
+ }
+
+ return mRevision.intValue();
+ }
+
+ public String getFormFactor() {
+ if (mFormFactor == null) {
+ mFormFactor = "Mobile";
+
+ NodeList formfactorDeclarations = mDocument.getElementsByTagName(TAG_FORMFACTOR);
+ if (formfactorDeclarations.getLength() > 0) {
+ Element element = (Element) formfactorDeclarations.item(0);
+ String formFactor = element.getAttribute(ATTR_VALUE);
+ if (formFactor != null && !formFactor.isEmpty()) {
+ mFormFactor = formFactor;
+ }
+ }
+ }
+ return mFormFactor;
+ }
+
+ public String getCategory() {
+ if (mCategory == null) {
+ mCategory = "";
+ NodeList categories = mDocument.getElementsByTagName(TAG_CATEGORY);
+ if (categories.getLength() > 0) {
+ Element element = (Element) categories.item(0);
+ String category = element.getAttribute(ATTR_VALUE);
+ if (category != null && !category.isEmpty()) {
+ mCategory = category;
+ }
+ }
+ }
+ return mCategory;
+ }
+
+ /**
+ * Returns a suitable icon wizard state instance if this wizard requests
+ * icons to be created, and null otherwise
+ *
+ * @return icon wizard state or null
+ */
+ @Nullable
+ public CreateAssetSetWizardState getIconState(IProject project) {
+ if (mIconState == null && !mNoIcons) {
+ NodeList icons = mDocument.getElementsByTagName(TAG_ICONS);
+ if (icons.getLength() < 1) {
+ mNoIcons = true;
+ return null;
+ }
+ Element icon = (Element) icons.item(0);
+
+ mIconState = new CreateAssetSetWizardState();
+ mIconState.project = project;
+
+ String typeString = getAttributeOrNull(icon, ATTR_TYPE);
+ if (typeString != null) {
+ typeString = typeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (AssetType type : AssetType.values()) {
+ if (typeString.equals(type.name())) {
+ mIconState.type = type;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown asset type %1$s", typeString);
+ }
+ }
+
+ mIconState.outputName = getAttributeOrNull(icon, ATTR_NAME);
+ if (mIconState.outputName != null) {
+ // Register parameter such that if it is referencing other values, it gets
+ // updated when other values are edited
+ Parameter outputParameter = new Parameter(this,
+ Parameter.Type.STRING, "_iconname", mIconState.outputName); //$NON-NLS-1$
+ getParameters().add(outputParameter);
+ }
+
+ RGB background = getRgb(icon, ATTR_BACKGROUND);
+ if (background != null) {
+ mIconState.background = background;
+ }
+ RGB foreground = getRgb(icon, ATTR_FOREGROUND);
+ if (foreground != null) {
+ mIconState.foreground = foreground;
+ }
+ String shapeString = getAttributeOrNull(icon, ATTR_SHAPE);
+ if (shapeString != null) {
+ shapeString = shapeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (GraphicGenerator.Shape shape : GraphicGenerator.Shape.values()) {
+ if (shapeString.equals(shape.name())) {
+ mIconState.shape = shape;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown shape %1$s", shapeString);
+ }
+ }
+ String trimString = getAttributeOrNull(icon, ATTR_TRIM);
+ if (trimString != null) {
+ mIconState.trim = Boolean.valueOf(trimString);
+ }
+ String paddingString = getAttributeOrNull(icon, ATTR_PADDING);
+ if (paddingString != null) {
+ mIconState.padding = Integer.parseInt(paddingString);
+ }
+ String sourceTypeString = getAttributeOrNull(icon, ATTR_SOURCE_TYPE);
+ if (sourceTypeString != null) {
+ sourceTypeString = sourceTypeString.toUpperCase(Locale.US);
+ boolean found = false;
+ for (SourceType type : SourceType.values()) {
+ if (sourceTypeString.equals(type.name())) {
+ mIconState.sourceType = type;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ AdtPlugin.log(null, "Unknown source type %1$s", sourceTypeString);
+ }
+ }
+ mIconState.clipartName = getAttributeOrNull(icon, ATTR_CLIPART_NAME);
+
+ String textString = getAttributeOrNull(icon, ATTR_TEXT);
+ if (textString != null) {
+ mIconState.text = textString;
+ }
+ }
+
+ return mIconState;
+ }
+
+ void updateIconName(List<Parameter> parameters, StringEvaluator evaluator) {
+ if (mIconState != null) {
+ NodeList icons = mDocument.getElementsByTagName(TAG_ICONS);
+ if (icons.getLength() < 1) {
+ return;
+ }
+ Element icon = (Element) icons.item(0);
+ String name = getAttributeOrNull(icon, ATTR_NAME);
+ if (name != null) {
+ mIconState.outputName = evaluator.evaluate(name, parameters);
+ }
+ }
+ }
+
+ private static RGB getRgb(@NonNull Element element, @NonNull String name) {
+ String colorString = getAttributeOrNull(element, name);
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ return ImageUtils.intToRgb(rgb);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static String getAttributeOrNull(@NonNull Element element, @NonNull String name) {
+ String value = element.getAttribute(name);
+ if (value != null && value.isEmpty()) {
+ return null;
+ }
+ return value;
+ }
+
+ @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 dependencies (as a list of pairs of names and revisions)
+ * required by this template
+ */
+ List<Pair<String, Integer>> getDependencies() {
+ if (mDependencies == null) {
+ NodeList elements = mDocument.getElementsByTagName(TAG_DEPENDENCY);
+ if (elements.getLength() == 0) {
+ return Collections.emptyList();
+ }
+
+ List<Pair<String, Integer>> dependencies = Lists.newArrayList();
+ for (int i = 0, n = elements.getLength(); i < n; i++) {
+ Element element = (Element) elements.item(i);
+ String name = element.getAttribute(ATTR_NAME);
+ int revision = -1;
+ String revisionString = element.getAttribute(ATTR_REVISION);
+ if (!revisionString.isEmpty()) {
+ revision = Integer.parseInt(revisionString);
+ }
+ dependencies.add(Pair.of(name, revision));
+ }
+ mDependencies = dependencies;
+ }
+
+ return mDependencies;
+ }
+
+ /** Returns the list of available parameters */
+ @NonNull
+ List<Parameter> getParameters() {
+ return mParameters;
+ }
+
+ /**
+ * Returns the parameter of the given id, or null if not found
+ *
+ * @param id the id of the target parameter
+ * @return the corresponding parameter, or null if not found
+ */
+ @Nullable
+ public Parameter getParameter(@NonNull String id) {
+ for (Parameter parameter : mParameters) {
+ if (id.equals(parameter.id)) {
+ return parameter;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns a default icon for templates */
+ static Image getDefaultTemplateIcon() {
+ return IconFactory.getInstance().getIcon("default_template"); //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java
new file mode 100644
index 000000000..c3d28fcf2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplatePreviewPage.java
@@ -0,0 +1,46 @@
+/*
+ * 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.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.internal.ui.refactoring.PreviewWizardPage;
+
+import java.util.List;
+
+@SuppressWarnings("restriction") // Refactoring UI
+class TemplatePreviewPage extends PreviewWizardPage {
+ private final NewTemplateWizardState mValues;
+
+ TemplatePreviewPage(NewTemplateWizardState values) {
+ super(true);
+ mValues = values;
+ setTitle("Preview");
+ setDescription("Optionally review pending changes");
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ List<Change> changes = mValues.computeChanges();
+ CompositeChange root = new CompositeChange("Create template",
+ changes.toArray(new Change[changes.size()]));
+ setChange(root);
+ }
+
+ super.setVisible(visible);
+ }
+}
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 000000000..e461d5597
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestPage.java
@@ -0,0 +1,161 @@
+/*
+ * 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 000000000..b3b1ef2f4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateTestWizard.java
@@ -0,0 +1,78 @@
+/*
+ * 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);
+ } else {
+ mValues.setTemplateLocation(file);
+ }
+
+ return mMainPage;
+ }
+ }
+
+ return super.getNextPage(page);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java
new file mode 100644
index 000000000..7ca32f91f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateWizard.java
@@ -0,0 +1,222 @@
+/*
+ * 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.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+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.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+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.jface.wizard.WizardPage;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.actions.WorkspaceModifyOperation;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.swing.SwingUtilities;
+
+abstract class TemplateWizard extends Wizard implements INewWizard {
+ private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$
+ protected IWorkbench mWorkbench;
+ private UpdateToolsPage mUpdatePage;
+ private InstallDependencyPage mDependencyPage;
+ private TemplatePreviewPage mPreviewPage;
+ protected ConfigureAssetSetPage mIconPage;
+
+ protected TemplateWizard() {
+ }
+
+ /** Should this wizard add an icon page? */
+ protected boolean shouldAddIconPage() {
+ return false;
+ }
+
+ @Override
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ mWorkbench = workbench;
+
+ setHelpAvailable(false);
+ ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+
+ if (!UpdateToolsPage.isUpToDate()) {
+ mUpdatePage = new UpdateToolsPage();
+ }
+
+ setNeedsProgressMonitor(true);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+
+ @Override
+ public void addPages() {
+ super.addPages();
+ if (mUpdatePage != null) {
+ addPage(mUpdatePage);
+ }
+ }
+
+ @Override
+ public IWizardPage getStartingPage() {
+ if (mUpdatePage != null && mUpdatePage.isPageComplete()) {
+ return getNextPage(mUpdatePage);
+ }
+
+ return super.getStartingPage();
+ }
+
+ protected WizardPage getPreviewPage(NewTemplateWizardState values) {
+ if (mPreviewPage == null) {
+ mPreviewPage = new TemplatePreviewPage(values);
+ addPage(mPreviewPage);
+ }
+
+ return mPreviewPage;
+ }
+
+ protected WizardPage getIconPage(CreateAssetSetWizardState iconState) {
+ if (mIconPage == null) {
+ mIconPage = new ConfigureAssetSetPage(iconState);
+ mIconPage.setTitle("Configure Icon");
+ addPage(mIconPage);
+ }
+
+ return mIconPage;
+ }
+
+ protected WizardPage getDependencyPage(TemplateMetadata template, boolean create) {
+ if (!create) {
+ return mDependencyPage;
+ }
+
+ if (mDependencyPage == null) {
+ mDependencyPage = new InstallDependencyPage();
+ addPage(mDependencyPage);
+ }
+ mDependencyPage.setTemplate(template);
+ return mDependencyPage;
+ }
+
+ /**
+ * Returns the project where the template is being inserted
+ *
+ * @return the project to insert the template into
+ */
+ @NonNull
+ protected abstract IProject getProject();
+
+ /**
+ * Returns the list of files to open, which might be empty. This method will
+ * only be called <b>after</b> {@link #computeChanges()} has been called.
+ *
+ * @return a list of files to open
+ */
+ @NonNull
+ protected abstract List<String> getFilesToOpen();
+
+ /**
+ * Returns the list of files to open, which might be empty. This method will
+ * only be called <b>after</b> {@link #computeChanges()} has been called.
+ *
+ * @return a list of files to open
+ */
+ @NonNull
+ protected abstract List<Runnable> getFinalizingActions();
+
+ /**
+ * Computes the changes to the {@link #getProject()} this template should
+ * perform
+ *
+ * @return the changes to perform
+ */
+ protected abstract List<Change> computeChanges();
+
+ protected boolean performFinish(IProgressMonitor monitor) throws InvocationTargetException {
+ List<Change> changes = computeChanges();
+ if (!changes.isEmpty()) {
+ monitor.beginTask("Creating template...", changes.size());
+ try {
+ CompositeChange composite = new CompositeChange("",
+ changes.toArray(new Change[changes.size()]));
+ composite.perform(monitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ // TBD: Is this necessary now that we're using IFile objects?
+ try {
+ getProject().refreshLocal(DEPTH_INFINITE, new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean performFinish() {
+ final AtomicBoolean success = new AtomicBoolean();
+ try {
+ getContainer().run(true, false, new IRunnableWithProgress() {
+ @Override
+ public void run(IProgressMonitor monitor) throws InvocationTargetException,
+ InterruptedException {
+ boolean ok = performFinish(monitor);
+ success.set(ok);
+ }
+ });
+
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ } catch (InterruptedException e) {
+ AdtPlugin.log(e, null);
+ return false;
+ }
+
+ if (success.get()) {
+ // Open the primary file/files
+ NewTemplateWizard.openFiles(getProject(), getFilesToOpen(), mWorkbench);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java
new file mode 100644
index 000000000..468a10c77
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TypedVariable.java
@@ -0,0 +1,50 @@
+package com.android.ide.eclipse.adt.internal.wizards.templates;
+
+import java.util.Locale;
+
+import org.xml.sax.Attributes;
+
+public class TypedVariable {
+ public enum Type {
+ STRING,
+ BOOLEAN,
+ INTEGER;
+
+ public static Type get(String name) {
+ if (name == null) {
+ return STRING;
+ }
+ try {
+ return valueOf(name.toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ System.err.println("Unexpected global type '" + name + "'");
+ System.err.println("Expected one of :");
+ for (Type s : Type.values()) {
+ System.err.println(" " + s.name().toLowerCase(Locale.US));
+ }
+ }
+
+ return STRING;
+ }
+ }
+
+ public static Object parseGlobal(Attributes attributes) {
+ String value = attributes.getValue(TemplateHandler.ATTR_VALUE);
+ Type type = Type.get(attributes.getValue(TemplateHandler.ATTR_TYPE));
+
+ switch (type) {
+ case STRING:
+ return value;
+ case BOOLEAN:
+ return Boolean.parseBoolean(value);
+ case INTEGER:
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return value;
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.java
new file mode 100644
index 000000000..5bbf449d4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/UpdateToolsPage.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.wizards.templates;
+
+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 UpdateToolsPage extends WizardPage implements SelectionListener {
+ private Button mInstallButton;
+ UpdateToolsPage() {
+ super("update");
+ setTitle("Update Tools");
+ validatePage();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ container.setLayout(new GridLayout(1, false));
+
+ Label label = new Label(container, SWT.WRAP);
+ GridData layoutData = new GridData(SWT.LEFT, SWT.TOP, true, true, 1, 1);
+ layoutData.widthHint = NewTemplatePage.WIZARD_PAGE_WIDTH - 50;
+ label.setLayoutData(layoutData);
+ label.setText(
+ "Your tools installation appears to be out of date (or not yet installed).\n" +
+ "\n" +
+ "This wizard depends on templates distributed with the Android SDK Tools.\n" +
+ "\n" +
+ "Please update the tools first (via Window > Android SDK Manager, or by " +
+ "using the \"android\" command in a terminal window). Note that on Windows " +
+ "you may need to restart the IDE, since there are some known problems where " +
+ "Windows locks the files held open by the running IDE, so the updater is " +
+ "unable to delete them in order to upgrade them.");
+
+ mInstallButton = new Button(container, SWT.NONE);
+ mInstallButton.setText("Check Again");
+ mInstallButton.addSelectionListener(this);
+ }
+
+ @Override
+ public boolean isPageComplete() {
+ return isUpToDate();
+ }
+
+ static boolean isUpToDate() {
+ return TemplateManager.getTemplateRootFolder() != null;
+ }
+
+ private void validatePage() {
+ boolean ok = isUpToDate();
+ setPageComplete(ok);
+ if (ok) {
+ setErrorMessage(null);
+ setMessage(null);
+ } else {
+ setErrorMessage("The tools need to be updated via the SDK Manager");
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mInstallButton) {
+ validatePage();
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}