diff options
author | Bob Badour <bbadour@google.com> | 2020-05-06 15:09:12 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-05-06 15:09:12 +0000 |
commit | f1a59c98333d28b04b74772f204bcc1df6e83634 (patch) | |
tree | fd845444b59dfc72656b7781596e0b1a0662c4c7 /eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates | |
parent | 14a008655dafe4ca1fc7d014a16a1c401761675c (diff) | |
parent | d58f8ba3b1869530926bd5f167103dfa161787a1 (diff) | |
download | sdk-f1a59c98333d28b04b74772f204bcc1df6e83634.tar.gz |
Merge "Revert "Remove unused project."" am: fc7cda06f5 am: d3c69fa48e am: d58f8ba3b1
Change-Id: I297730ce4f9da2bf30dde696439ab825cd642b00
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates')
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 + * < and & 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) { + } +} |