diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards')
75 files changed, 20974 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java new file mode 100644 index 000000000..4d3870b86 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportAction.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2007 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.actions; + +import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; +import com.android.ide.eclipse.adt.internal.project.ExportHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +public class ExportAction implements IObjectActionDelegate { + + private ISelection mSelection; + private Shell mShell; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + @Override + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mShell = targetPart.getSite().getShell(); + } + + @Override + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + // get the unique selected item. + if (selection.size() == 1) { + Object element = selection.getFirstElement(); + + // get the project object from it. + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); + } + + // and finally do the action + if (project != null) { + if (!EclipseLintRunner.runLintOnExport(mShell, project)) { + return; + } + + ProjectState state = Sdk.getProjectState(project); + if (state.isLibrary()) { + MessageDialog.openError(mShell, "Android Export", + "Android library projects cannot be exported."); + } else { + ExportHelper.exportUnsignedReleaseApk(project); + } + } + } + } + } + + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java new file mode 100644 index 000000000..673d9569f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/ExportWizardAction.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2008 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.actions; + +import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; + +public class ExportWizardAction implements IObjectActionDelegate { + + private ISelection mSelection; + private IWorkbench mWorkbench; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + @Override + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); + } + + @Override + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + + // get the unique selected item. + if (selection.size() == 1) { + Object element = selection.getFirstElement(); + + // get the project object from it. + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); + } + + // and finally do the action + if (project != null) { + if (!EclipseLintRunner.runLintOnExport( + mWorkbench.getActiveWorkbenchWindow().getShell(), project)) { + return; + } + + ProjectState state = Sdk.getProjectState(project); + if (state.isLibrary()) { + MessageDialog.openError(mWorkbench.getDisplay().getActiveShell(), + "Android Export", + "Android library projects cannot be exported."); + } else { + // call the export wizard on the current selection. + ExportWizard wizard = new ExportWizard(); + wizard.init(mWorkbench, selection); + WizardDialog dialog = new WizardDialog( + mWorkbench.getDisplay().getActiveShell(), wizard); + dialog.open(); + } + } + } + } + } + + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java new file mode 100644 index 000000000..38f4768b1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewProjectAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009 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.actions; + +import com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project". + * It displays the Android New Project wizard to create a new Android Project (not a test project). + * + * @see NewTestProjectAction + */ +public class NewProjectAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewProjectWizard(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java new file mode 100755 index 000000000..c8e45ef1a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewTestProjectAction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 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.actions; + +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewTestProjectWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Test Project". + * It displays the Android New Project wizard to create a new Test Project. + */ +public class NewTestProjectAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewTestProjectWizard(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java new file mode 100644 index 000000000..ba349c30a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/NewXmlFileAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009 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.actions; + +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project" or for the + * project > Android Project context menu. + * + * It displays the Android New XML file wizard. + */ +public class NewXmlFileAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewXmlFileWizard(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java new file mode 100644 index 000000000..a3e6135e5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/actions/OpenWizardAction.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2009 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.actions; + +import com.android.ide.eclipse.adt.internal.ui.IUpdateWizardDialog; +import com.android.ide.eclipse.adt.internal.ui.WizardDialogEx; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.IWorkbenchWizard; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.IWorkbenchHelpContextIds; +import org.eclipse.ui.internal.LegacyResourceSupport; +import org.eclipse.ui.internal.actions.NewWizardShortcutAction; +import org.eclipse.ui.internal.util.Util; + +/** + * An abstract action that displays one of our wizards. + * Derived classes must provide the actual wizard to display. + */ +/*package*/ abstract class OpenWizardAction + implements IWorkbenchWindowActionDelegate, IObjectActionDelegate { + + /** + * The wizard dialog width, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_WIDTH = 500; + + /** + * The wizard dialog height, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_HEIGHT = 500; + + /** The wizard that was created by {@link #run(IAction)}. */ + private IWorkbenchWizard mWizard; + /** The result from the dialog */ + private int mDialogResult; + + private ISelection mSelection; + private IWorkbench mWorkbench; + + /** Returns the wizard that was created by {@link #run(IAction)}. */ + public IWorkbenchWizard getWizard() { + return mWizard; + } + + /** Returns the result from {@link Dialog#open()}, available after + * the completion of {@link #run(IAction)}. */ + public int getDialogResult() { + return mDialogResult; + } + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose() + */ + @Override + public void dispose() { + // pass + } + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow) + */ + @Override + public void init(IWorkbenchWindow window) { + // pass + } + + /** + * Opens and display the Android New Project Wizard. + * <p/> + * Most of this implementation is extracted from {@link NewWizardShortcutAction#run()}. + * + * @param action The action that got us here. Can be null when used internally. + * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction) + */ + @Override + public void run(IAction action) { + + // get the workbench and the current window + IWorkbench workbench = mWorkbench != null ? mWorkbench : PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + + // This code from NewWizardShortcutAction#run() gets the current window selection + // and converts it to a workbench structured selection for the wizard, if possible. + ISelection selection = mSelection; + if (selection == null) { + selection = window.getSelectionService().getSelection(); + } + + IStructuredSelection selectionToPass = StructuredSelection.EMPTY; + if (selection instanceof IStructuredSelection) { + selectionToPass = (IStructuredSelection) selection; + } else { + // Build the selection from the IFile of the editor + IWorkbenchPart part = window.getPartService().getActivePart(); + if (part instanceof IEditorPart) { + IEditorInput input = ((IEditorPart) part).getEditorInput(); + Class<?> fileClass = LegacyResourceSupport.getFileClass(); + if (input != null && fileClass != null) { + Object file = Util.getAdapter(input, fileClass); + if (file != null) { + selectionToPass = new StructuredSelection(file); + } + } + } + } + + // Create the wizard and initialize it with the selection + mWizard = instanciateWizard(action); + mWizard.init(workbench, selectionToPass); + + // It's not visible yet until a dialog is created and opened + Shell parent = window.getShell(); + WizardDialogEx dialog = new WizardDialogEx(parent, mWizard); + dialog.create(); + + if (mWizard instanceof IUpdateWizardDialog) { + ((IUpdateWizardDialog) mWizard).updateWizardDialog(dialog); + } + + // This code comes straight from NewWizardShortcutAction#run() + Point defaultSize = dialog.getShell().getSize(); + dialog.getShell().setSize( + Math.max(SIZING_WIZARD_WIDTH, defaultSize.x), + Math.max(SIZING_WIZARD_HEIGHT, defaultSize.y)); + window.getWorkbench().getHelpSystem().setHelp(dialog.getShell(), + IWorkbenchHelpContextIds.NEW_WIZARD_SHORTCUT); + + mDialogResult = dialog.open(); + } + + /** + * Called by {@link #run(IAction)} to instantiate the actual wizard. + * + * @param action The action parameter from {@link #run(IAction)}. + * This can be null. + * @return A new wizard instance. Must not be null. + */ + protected abstract IWorkbenchWizard instanciateWizard(IAction action); + + /* (non-Javadoc) + * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection) + */ + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } + + /* (non-Javadoc) + * @see org.eclipse.ui.IObjectActionDelegate#setActivePart(org.eclipse.jface.action.IAction, org.eclipse.ui.IWorkbenchPart) + */ + @Override + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java new file mode 100644 index 000000000..170da6d33 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java @@ -0,0 +1,626 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.project.ExportHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.BuildToolInfo.PathId; +import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; +import com.android.sdklib.internal.build.KeystoreHelper; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IExportWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.security.KeyStore; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Export wizard to export an apk signed with a release key/certificate. + */ +public final class ExportWizard extends Wizard implements IExportWizard { + + private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ + + private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$ + private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$ + private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$ + private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$ + private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$ + + static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$ + static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$ + static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$ + + static final int APK_FILE_SOURCE = 0; + static final int APK_FILE_DEST = 1; + static final int APK_COUNT = 2; + + /** + * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback. + */ + static abstract class ExportWizardPage extends WizardPage { + + /** bit mask constant for project data change event */ + protected static final int DATA_PROJECT = 0x001; + /** bit mask constant for keystore data change event */ + protected static final int DATA_KEYSTORE = 0x002; + /** bit mask constant for key data change event */ + protected static final int DATA_KEY = 0x004; + + protected static final VerifyListener sPasswordVerifier = new VerifyListener() { + @Override + public void verifyText(VerifyEvent e) { + // verify the characters are valid for password. + int len = e.text.length(); + + // first limit to 127 characters max + if (len + ((Text)e.getSource()).getText().length() > 127) { + e.doit = false; + return; + } + + // now only take non control characters + for (int i = 0 ; i < len ; i++) { + if (e.text.charAt(i) < 32) { + e.doit = false; + return; + } + } + } + }; + + /** + * Bit mask indicating what changed while the page was hidden. + * @see #DATA_PROJECT + * @see #DATA_KEYSTORE + * @see #DATA_KEY + */ + protected int mProjectDataChanged = 0; + + ExportWizardPage(String name) { + super(name); + } + + abstract void onShow(); + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + onShow(); + mProjectDataChanged = 0; + } + } + + final void projectDataChanged(int changeMask) { + mProjectDataChanged |= changeMask; + } + + /** + * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a + * {@link Throwable} object. + */ + protected void onException(Throwable t) { + String message = getExceptionMessage(t); + + setErrorMessage(message); + setPageComplete(false); + } + } + + private ExportWizardPage mPages[] = new ExportWizardPage[5]; + + private IProject mProject; + + private String mKeystore; + private String mKeystorePassword; + private boolean mKeystoreCreationMode; + + private String mKeyAlias; + private String mKeyPassword; + private int mValidity; + private String mDName; + + private PrivateKey mPrivateKey; + private X509Certificate mCertificate; + + private File mDestinationFile; + + private ExportWizardPage mKeystoreSelectionPage; + private ExportWizardPage mKeyCreationPage; + private ExportWizardPage mKeySelectionPage; + private ExportWizardPage mKeyCheckPage; + + private boolean mKeyCreationMode; + + private List<String> mExistingAliases; + + public ExportWizard() { + setHelpAvailable(false); // TODO have help + setWindowTitle("Export Android Application"); + setImageDescriptor(); + } + + @Override + public void addPages() { + addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK)); + addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this, + PAGE_KEYSTORE_SELECTION)); + addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION)); + addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION)); + addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK)); + } + + @Override + public boolean performFinish() { + // save the properties + ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore); + ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias); + ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, + mDestinationFile.getAbsolutePath()); + + // run the export in an UI runnable. + IWorkbench workbench = PlatformUI.getWorkbench(); + final boolean[] result = new boolean[1]; + try { + workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() { + /** + * Run the export. + * @throws InvocationTargetException + * @throws InterruptedException + */ + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, + InterruptedException { + try { + result[0] = doExport(monitor); + } finally { + monitor.done(); + } + } + }); + } catch (InvocationTargetException e) { + return false; + } catch (InterruptedException e) { + return false; + } + + return result[0]; + } + + private boolean doExport(IProgressMonitor monitor) { + try { + // if needed, create the keystore and/or key. + if (mKeystoreCreationMode || mKeyCreationMode) { + final ArrayList<String> output = new ArrayList<String>(); + boolean createdStore = KeystoreHelper.createNewStore( + mKeystore, + null /*storeType*/, + mKeystorePassword, + mKeyAlias, + mKeyPassword, + mDName, + mValidity, + new IKeyGenOutput() { + @Override + public void err(String message) { + output.add(message); + } + @Override + public void out(String message) { + output.add(message); + } + }); + + if (createdStore == false) { + // keystore creation error! + displayError(output.toArray(new String[output.size()])); + return false; + } + + // keystore is created, now load the private key and certificate. + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mKeystore); + keyStore.load(fis, mKeystorePassword.toCharArray()); + fis.close(); + PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( + mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray())); + + if (entry != null) { + mPrivateKey = entry.getPrivateKey(); + mCertificate = (X509Certificate)entry.getCertificate(); + + AdtPlugin.printToConsole(mProject, + String.format("New keystore %s has been created.", + mDestinationFile.getAbsolutePath()), + "Certificate fingerprints:", + String.format(" MD5 : %s", getCertMd5Fingerprint()), + String.format(" SHA1: %s", getCertSha1Fingerprint())); + + } else { + // this really shouldn't happen since we now let the user choose the key + // from a list read from the store. + displayError("Could not find key"); + return false; + } + } + + // check the private key/certificate again since it may have been created just above. + if (mPrivateKey != null && mCertificate != null) { + // check whether we can run zipalign. + boolean runZipAlign = false; + + ProjectState projectState = Sdk.getProjectState(mProject); + BuildToolInfo buildToolInfo = ExportHelper.getBuildTools(projectState); + + String zipAlignPath = buildToolInfo.getPath(PathId.ZIP_ALIGN); + runZipAlign = zipAlignPath != null && new File(zipAlignPath).isFile(); + + File apkExportFile = mDestinationFile; + if (runZipAlign) { + // create a temp file for the original export. + apkExportFile = File.createTempFile("androidExport_", ".apk"); + } + + // export the signed apk. + ExportHelper.exportReleaseApk(mProject, apkExportFile, + mPrivateKey, mCertificate, monitor); + + // align if we can + if (runZipAlign) { + String message = zipAlign(zipAlignPath, apkExportFile, mDestinationFile); + if (message != null) { + displayError(message); + return false; + } + } else { + AdtPlugin.displayWarning("Export Wizard", + "The zipalign tool was not found in the SDK.\n\n" + + "Please update to the latest SDK and re-export your application\n" + + "or run zipalign manually.\n\n" + + "Aligning applications allows Android to use application resources\n" + + "more efficiently."); + } + + return true; + } + } catch (Throwable t) { + displayError(t); + } + + return false; + } + + @Override + public boolean canFinish() { + // check if we have the apk to resign, the destination location, and either + // a private key/certificate or the creation mode. In creation mode, unless + // all the key/keystore info is valid, the user cannot reach the last page, so there's + // no need to check them again here. + return ((mPrivateKey != null && mCertificate != null) + || mKeystoreCreationMode || mKeyCreationMode) && + mDestinationFile != null; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, + * org.eclipse.jface.viewers.IStructuredSelection) + */ + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + // get the project from the selection + Object selected = selection.getFirstElement(); + + if (selected instanceof IProject) { + mProject = (IProject)selected; + } else if (selected instanceof IAdaptable) { + IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class); + if (r != null) { + mProject = r.getProject(); + } + } + } + + ExportWizardPage getKeystoreSelectionPage() { + return mKeystoreSelectionPage; + } + + ExportWizardPage getKeyCreationPage() { + return mKeyCreationPage; + } + + ExportWizardPage getKeySelectionPage() { + return mKeySelectionPage; + } + + ExportWizardPage getKeyCheckPage() { + return mKeyCheckPage; + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + + IProject getProject() { + return mProject; + } + + void setProject(IProject project) { + mProject = project; + + updatePageOnChange(ExportWizardPage.DATA_PROJECT); + } + + void setKeystore(String path) { + mKeystore = path; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + String getKeystore() { + return mKeystore; + } + + void setKeystoreCreationMode(boolean createStore) { + mKeystoreCreationMode = createStore; + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + boolean getKeystoreCreationMode() { + return mKeystoreCreationMode; + } + + + void setKeystorePassword(String password) { + mKeystorePassword = password; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + String getKeystorePassword() { + return mKeystorePassword; + } + + void setKeyCreationMode(boolean createKey) { + mKeyCreationMode = createKey; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + boolean getKeyCreationMode() { + return mKeyCreationMode; + } + + void setExistingAliases(List<String> aliases) { + mExistingAliases = aliases; + } + + List<String> getExistingAliases() { + return mExistingAliases; + } + + void setKeyAlias(String name) { + mKeyAlias = name; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getKeyAlias() { + return mKeyAlias; + } + + void setKeyPassword(String password) { + mKeyPassword = password; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getKeyPassword() { + return mKeyPassword; + } + + void setValidity(int validity) { + mValidity = validity; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + int getValidity() { + return mValidity; + } + + void setDName(String dName) { + mDName = dName; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getDName() { + return mDName; + } + + String getCertSha1Fingerprint() { + return FingerprintUtils.getFingerprint(mCertificate, "SHA1"); + } + + String getCertMd5Fingerprint() { + return FingerprintUtils.getFingerprint(mCertificate, "MD5"); + } + + void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + void setDestination(File destinationFile) { + mDestinationFile = destinationFile; + } + + void resetDestination() { + mDestinationFile = null; + } + + void updatePageOnChange(int changeMask) { + for (ExportWizardPage page : mPages) { + page.projectDataChanged(changeMask); + } + } + + private void displayError(String... messages) { + String message = null; + if (messages.length == 1) { + message = messages[0]; + } else { + StringBuilder sb = new StringBuilder(messages[0]); + for (int i = 1; i < messages.length; i++) { + sb.append('\n'); + sb.append(messages[i]); + } + + message = sb.toString(); + } + + AdtPlugin.displayError("Export Wizard", message); + } + + private void displayError(Throwable t) { + String message = getExceptionMessage(t); + displayError(message); + + AdtPlugin.log(t, "Export Wizard Error"); + } + + /** + * Executes zipalign + * @param zipAlignPath location of the zipalign too + * @param source file to zipalign + * @param destination where to write the resulting file + * @return null if success, the error otherwise + * @throws IOException + */ + private String zipAlign(String zipAlignPath, File source, File destination) throws IOException { + // command line: zipaling -f 4 tmp destination + String[] command = new String[5]; + command[0] = zipAlignPath; + command[1] = "-f"; //$NON-NLS-1$ + command[2] = "4"; //$NON-NLS-1$ + command[3] = source.getAbsolutePath(); + command[4] = destination.getAbsolutePath(); + + Process process = Runtime.getRuntime().exec(command); + final ArrayList<String> output = new ArrayList<String>(); + try { + final IProject project = getProject(); + + int status = GrabProcessOutput.grabProcessOutput( + process, + Wait.WAIT_FOR_READERS, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + if (line != null) { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, + project, line); + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + output.add(line); + } + } + }); + + if (status != 0) { + // build a single message from the array list + StringBuilder sb = new StringBuilder("Error while running zipalign:"); + for (String msg : output) { + sb.append('\n'); + sb.append(msg); + } + + return sb.toString(); + } + } catch (InterruptedException e) { + // ? + } + return null; + } + + /** + * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns + * <code>null</code>, the method is called again on the cause of the Throwable object. + * <p/>If no Throwable in the chain has a valid message, the canonical name of the first + * exception is returned. + */ + static String getExceptionMessage(Throwable t) { + String message = t.getMessage(); + if (message == null) { + // no error info? get the stack call to display it + // At least that'll give us a better bug report. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.printStackTrace(new PrintStream(baos)); + message = baos.toString(); + } + + return message; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java new file mode 100644 index 000000000..c17f43e38 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Rectangle; +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.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.widgets.FormText; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; + +/** + * Final page of the wizard that checks the key and ask for the ouput location. + */ +final class KeyCheckPage extends ExportWizardPage { + + private static final int REQUIRED_YEARS = 25; + + private static final String VALIDITY_WARNING = + "<p>Make sure the certificate is valid for the planned lifetime of the product.</p>" + + "<p>If the certificate expires, you will be forced to sign your application with " + + "a different one.</p>" + + "<p>Applications cannot be upgraded if their certificate changes from " + + "one version to another, forcing a full uninstall/install, which will make " + + "the user lose his/her data.</p>" + + "<p>Google Play(Android Market) currently requires certificates to be valid " + + "until 2033.</p>"; + + private final ExportWizard mWizard; + private PrivateKey mPrivateKey; + private X509Certificate mCertificate; + private Text mDestination; + private boolean mFatalSigningError; + private FormText mDetailText; + private ScrolledComposite mScrolledComposite; + + private String mKeyDetails; + private String mDestinationDetails; + + protected KeyCheckPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Destination and key/certificate checks"); + setDescription(""); // TODO + } + + @Override + public void createControl(Composite parent) { + setErrorMessage(null); + setMessage(null); + + // build the ui. + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + gl.verticalSpacing *= 3; + composite.setLayout(gl); + + GridData gd; + + new Label(composite, SWT.NONE).setText("Destination APK file:"); + mDestination = new Text(composite, SWT.BORDER); + mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mDestination.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + onDestinationChange(false /*forceDetailUpdate*/); + } + }); + final Button browseButton = new Button(composite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE); + + fileDialog.setText("Destination file name"); + // get a default apk name based on the project + String filename = ProjectHelper.getApkFilename(mWizard.getProject(), + null /*config*/); + fileDialog.setFileName(filename); + + String saveLocation = fileDialog.open(); + if (saveLocation != null) { + mDestination.setText(saveLocation); + } + } + }); + + mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL); + mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.horizontalSpan = 3; + mScrolledComposite.setExpandHorizontal(true); + mScrolledComposite.setExpandVertical(true); + + mDetailText = new FormText(mScrolledComposite, SWT.NONE); + mScrolledComposite.setContent(mDetailText); + + mScrolledComposite.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + updateScrolling(); + } + }); + + setControl(composite); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & DATA_PROJECT) != 0) { + // reset the destination from the content of the project + IProject project = mWizard.getProject(); + + String destination = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_DESTINATION); + if (destination != null) { + mDestination.setText(destination); + } + } + + // if anything change we basically reload the data. + if (mProjectDataChanged != 0) { + mFatalSigningError = false; + + // reset the wizard with no key/cert to make it not finishable, unless a valid + // key/cert is found. + mWizard.setSigningInfo(null, null); + mPrivateKey = null; + mCertificate = null; + mKeyDetails = null; + + if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) { + int validity = mWizard.getValidity(); + StringBuilder sb = new StringBuilder( + String.format("<p>Certificate expires in %d years.</p>", + validity)); + + if (validity < REQUIRED_YEARS) { + sb.append(VALIDITY_WARNING); + } + + mKeyDetails = sb.toString(); + } else { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mWizard.getKeystore()); + keyStore.load(fis, mWizard.getKeystorePassword().toCharArray()); + fis.close(); + PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( + mWizard.getKeyAlias(), + new KeyStore.PasswordProtection( + mWizard.getKeyPassword().toCharArray())); + + if (entry != null) { + mPrivateKey = entry.getPrivateKey(); + mCertificate = (X509Certificate)entry.getCertificate(); + } else { + setErrorMessage("Unable to find key."); + + setPageComplete(false); + } + } catch (FileNotFoundException e) { + // this was checked at the first previous step and will not happen here, unless + // the file was removed during the export wizard execution. + onException(e); + } catch (KeyStoreException e) { + onException(e); + } catch (NoSuchAlgorithmException e) { + onException(e); + } catch (UnrecoverableEntryException e) { + onException(e); + } catch (CertificateException e) { + onException(e); + } catch (IOException e) { + onException(e); + } + + if (mPrivateKey != null && mCertificate != null) { + Calendar expirationCalendar = Calendar.getInstance(); + expirationCalendar.setTime(mCertificate.getNotAfter()); + Calendar today = Calendar.getInstance(); + + if (expirationCalendar.before(today)) { + mKeyDetails = String.format( + "<p>Certificate expired on %s</p>", + mCertificate.getNotAfter().toString()); + + // fatal error = nothing can make the page complete. + mFatalSigningError = true; + + setErrorMessage("Certificate is expired."); + setPageComplete(false); + } else { + // valid, key/cert: put it in the wizard so that it can be finished + mWizard.setSigningInfo(mPrivateKey, mCertificate); + + StringBuilder sb = new StringBuilder(String.format( + "<p>Certificate expires on %s.</p>", + mCertificate.getNotAfter().toString())); + + int expirationYear = expirationCalendar.get(Calendar.YEAR); + int thisYear = today.get(Calendar.YEAR); + + if (thisYear + REQUIRED_YEARS < expirationYear) { + // do nothing + } else { + if (expirationYear == thisYear) { + sb.append("<p>The certificate expires this year.</p>"); + } else { + int count = expirationYear-thisYear; + sb.append(String.format( + "<p>The Certificate expires in %1$s %2$s.</p>", + count, count == 1 ? "year" : "years")); + } + sb.append(VALIDITY_WARNING); + } + + // show certificate fingerprints + String sha1 = mWizard.getCertSha1Fingerprint(); + String md5 = mWizard.getCertMd5Fingerprint(); + + sb.append("<p></p>" /*blank line*/); + sb.append("<p>Certificate fingerprints:</p>"); + sb.append(String.format("<li>MD5 : %s</li>", md5)); + sb.append(String.format("<li>SHA1: %s</li>", sha1)); + sb.append("<p></p>" /*blank line*/); + + mKeyDetails = sb.toString(); + } + } else { + // fatal error = nothing can make the page complete. + mFatalSigningError = true; + } + } + } + + onDestinationChange(true /*forceDetailUpdate*/); + } + + /** + * Callback for destination field edition + * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal + * error has happened in the signing. + */ + private void onDestinationChange(boolean forceDetailUpdate) { + if (mFatalSigningError == false) { + // reset messages for now. + setErrorMessage(null); + setMessage(null); + + String path = mDestination.getText().trim(); + + if (path.length() == 0) { + setErrorMessage("Enter destination for the APK file."); + // reset canFinish in the wizard. + mWizard.resetDestination(); + setPageComplete(false); + return; + } + + File file = new File(path); + if (file.isDirectory()) { + setErrorMessage("Destination is a directory."); + // reset canFinish in the wizard. + mWizard.resetDestination(); + setPageComplete(false); + return; + } + + File parentFolder = file.getParentFile(); + if (parentFolder == null || parentFolder.isDirectory() == false) { + setErrorMessage("Not a valid directory."); + // reset canFinish in the wizard. + mWizard.resetDestination(); + setPageComplete(false); + return; + } + + if (file.isFile()) { + mDestinationDetails = "<li>WARNING: destination file already exists</li>"; + setMessage("Destination file already exists.", WARNING); + } + + // no error, set the destination in the wizard. + mWizard.setDestination(file); + setPageComplete(true); + + updateDetailText(); + } else if (forceDetailUpdate) { + updateDetailText(); + } + } + + /** + * Updates the scrollbar to match the content of the {@link FormText} or the new size + * of the {@link ScrolledComposite}. + */ + private void updateScrolling() { + if (mDetailText != null) { + Rectangle r = mScrolledComposite.getClientArea(); + mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT)); + mScrolledComposite.layout(); + } + } + + private void updateDetailText() { + StringBuilder sb = new StringBuilder("<form>"); + if (mKeyDetails != null) { + sb.append(mKeyDetails); + } + + if (mDestinationDetails != null && mFatalSigningError == false) { + sb.append(mDestinationDetails); + } + + sb.append("</form>"); + + mDetailText.setText(sb.toString(), true /* parseTags */, + true /* expandURLs */); + + mDetailText.getParent().layout(); + + updateScrolling(); + } + + @Override + protected void onException(Throwable t) { + super.onException(t); + + mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t)); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java new file mode 100644 index 000000000..aea94ad8d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCreationPage.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.List; + +/** + * Key creation page. + */ +final class KeyCreationPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Text mAlias; + private Text mKeyPassword; + private Text mKeyPassword2; + private Text mCnField; + private boolean mDisableOnChange = false; + private Text mOuField; + private Text mOField; + private Text mLField; + private Text mStField; + private Text mCField; + private String mDName; + private int mValidity = 0; + private List<String> mExistingAliases; + + + protected KeyCreationPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Key Creation"); + setDescription(""); // TODO? + } + + @Override + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(2, false); + composite.setLayout(gl); + + GridData gd; + + new Label(composite, SWT.NONE).setText("Alias:"); + mAlias = new Text(composite, SWT.BORDER); + mAlias.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Password:"); + mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeyPassword.addVerifyListener(sPasswordVerifier); + + new Label(composite, SWT.NONE).setText("Confirm:"); + mKeyPassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeyPassword2.addVerifyListener(sPasswordVerifier); + + new Label(composite, SWT.NONE).setText("Validity (years):"); + final Text validityText = new Text(composite, SWT.BORDER); + validityText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + validityText.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent e) { + // check for digit only. + for (int i = 0 ; i < e.text.length(); i++) { + char letter = e.text.charAt(i); + if (letter < '0' || letter > '9') { + e.doit = false; + return; + } + } + } + }); + + new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData( + gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + + new Label(composite, SWT.NONE).setText("First and Last Name:"); + mCnField = new Text(composite, SWT.BORDER); + mCnField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Organizational Unit:"); + mOuField = new Text(composite, SWT.BORDER); + mOuField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Organization:"); + mOField = new Text(composite, SWT.BORDER); + mOField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("City or Locality:"); + mLField = new Text(composite, SWT.BORDER); + mLField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("State or Province:"); + mStField = new Text(composite, SWT.BORDER); + mStField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Country Code (XX):"); + mCField = new Text(composite, SWT.BORDER); + mCField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mAlias.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mWizard.setKeyAlias(mAlias.getText().trim()); + onChange(); + } + }); + mKeyPassword.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mWizard.setKeyPassword(mKeyPassword.getText()); + onChange(); + } + }); + mKeyPassword2.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + onChange(); + } + }); + + validityText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + try { + mValidity = Integer.parseInt(validityText.getText()); + } catch (NumberFormatException e2) { + // this should only happen if the text field is empty due to the verifyListener. + mValidity = 0; + } + mWizard.setValidity(mValidity); + onChange(); + } + }); + + ModifyListener dNameListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + onDNameChange(); + } + }; + + mCnField.addModifyListener(dNameListener); + mOuField.addModifyListener(dNameListener); + mOField.addModifyListener(dNameListener); + mLField.addModifyListener(dNameListener); + mStField.addModifyListener(dNameListener); + mCField.addModifyListener(dNameListener); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) { + // reset the keystore/alias from the content of the project + IProject project = mWizard.getProject(); + + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + String alias = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_ALIAS); + if (alias != null) { + mAlias.setText(alias); + } + + // get the existing list of keys if applicable + if (mWizard.getKeyCreationMode()) { + mExistingAliases = mWizard.getExistingAliases(); + } else { + mExistingAliases = null; + } + + // reset the passwords + mKeyPassword.setText(""); //$NON-NLS-1$ + mKeyPassword2.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } + } + + @Override + public IWizardPage getPreviousPage() { + if (mWizard.getKeyCreationMode()) { // this means we create a key from an existing store + return mWizard.getKeySelectionPage(); + } + + return mWizard.getKeystoreSelectionPage(); + } + + @Override + public IWizardPage getNextPage() { + return mWizard.getKeyCheckPage(); + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + if (mAlias.getText().trim().length() == 0) { + setErrorMessage("Enter key alias."); + setPageComplete(false); + return; + } else if (mExistingAliases != null) { + // we cannot use indexOf, because we need to do a case-insensitive check + String keyAlias = mAlias.getText().trim(); + for (String alias : mExistingAliases) { + if (alias.equalsIgnoreCase(keyAlias)) { + setErrorMessage("Key alias already exists in keystore."); + setPageComplete(false); + return; + } + } + } + + String value = mKeyPassword.getText(); + if (value.length() == 0) { + setErrorMessage("Enter key password."); + setPageComplete(false); + return; + } else if (value.length() < 6) { + setErrorMessage("Key password is too short - must be at least 6 characters."); + setPageComplete(false); + return; + } + + if (value.equals(mKeyPassword2.getText()) == false) { + setErrorMessage("Key passwords don't match."); + setPageComplete(false); + return; + } + + if (mValidity == 0) { + setErrorMessage("Key certificate validity is required."); + setPageComplete(false); + return; + } else if (mValidity < 25) { + setMessage("A 25 year certificate validity is recommended.", WARNING); + } else if (mValidity > 1000) { + setErrorMessage("Key certificate validity must be between 1 and 1000 years."); + setPageComplete(false); + return; + } + + if (mDName == null || mDName.length() == 0) { + setErrorMessage("At least one Certificate issuer field is required to be non-empty."); + setPageComplete(false); + return; + } + + setPageComplete(true); + } + + /** + * Handles changes in the DName fields. + */ + private void onDNameChange() { + StringBuilder sb = new StringBuilder(); + + buildDName("CN", mCnField, sb); + buildDName("OU", mOuField, sb); + buildDName("O", mOField, sb); + buildDName("L", mLField, sb); + buildDName("ST", mStField, sb); + buildDName("C", mCField, sb); + + mDName = sb.toString(); + mWizard.setDName(mDName); + + onChange(); + } + + /** + * Builds the distinguished name string with the provided {@link StringBuilder}. + * @param prefix the prefix of the entry. + * @param textField The {@link Text} field containing the entry value. + * @param sb the string builder containing the dname. + */ + private void buildDName(String prefix, Text textField, StringBuilder sb) { + if (textField != null) { + String value = textField.getText().trim(); + if (value.length() > 0) { + if (sb.length() > 0) { + sb.append(","); + } + + sb.append(prefix); + sb.append('='); + sb.append(value); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java new file mode 100644 index 000000000..604a208e6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeySelectionPage.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Enumeration; + +/** + * Key Selection Page. This is used when an existing keystore is used. + */ +final class KeySelectionPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Label mKeyAliasesLabel; + private Combo mKeyAliases; + private Label mKeyPasswordLabel; + private Text mKeyPassword; + private boolean mDisableOnChange = false; + private Button mUseExistingKey; + private Button mCreateKey; + + protected KeySelectionPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Key alias selection"); + setDescription(""); // TODO + } + + @Override + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + composite.setLayout(gl); + + GridData gd; + + mUseExistingKey = new Button(composite, SWT.RADIO); + mUseExistingKey.setText("Use existing key"); + mUseExistingKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + mUseExistingKey.setSelection(true); + + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = 0; + gd.widthHint = 50; + mKeyAliasesLabel = new Label(composite, SWT.NONE); + mKeyAliasesLabel.setText("Alias:"); + mKeyAliases = new Combo(composite, SWT.READ_ONLY); + mKeyAliases.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = 0; + gd.widthHint = 50; + mKeyPasswordLabel = new Label(composite, SWT.NONE); + mKeyPasswordLabel.setText("Password:"); + mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mCreateKey = new Button(composite, SWT.RADIO); + mCreateKey.setText("Create new key"); + mCreateKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mUseExistingKey.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mWizard.setKeyCreationMode(!mUseExistingKey.getSelection()); + enableWidgets(); + onChange(); + } + }); + + mKeyAliases.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mWizard.setKeyAlias(mKeyAliases.getItem(mKeyAliases.getSelectionIndex())); + onChange(); + } + }); + + mKeyPassword.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mWizard.setKeyPassword(mKeyPassword.getText()); + onChange(); + } + }); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) { + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + // reset the alias from the content of the project + try { + // reset to using a key + mWizard.setKeyCreationMode(false); + mUseExistingKey.setSelection(true); + mCreateKey.setSelection(false); + enableWidgets(); + + // remove the content of the alias combo always and first, in case the + // keystore password is wrong + mKeyAliases.removeAll(); + + // get the alias list (also used as a keystore password test) + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mWizard.getKeystore()); + keyStore.load(fis, mWizard.getKeystorePassword().toCharArray()); + fis.close(); + + Enumeration<String> aliases = keyStore.aliases(); + + // get the alias from the project previous export, and look for a match as + // we add the aliases to the combo. + IProject project = mWizard.getProject(); + + String keyAlias = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_ALIAS); + + ArrayList<String> aliasList = new ArrayList<String>(); + + int selection = -1; + int count = 0; + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + mKeyAliases.add(alias); + aliasList.add(alias); + if (selection == -1 && alias.equalsIgnoreCase(keyAlias)) { + selection = count; + } + count++; + } + + mWizard.setExistingAliases(aliasList); + + if (selection != -1) { + mKeyAliases.select(selection); + + // since a match was found and is selected, we need to give it to + // the wizard as well + mWizard.setKeyAlias(keyAlias); + } else { + mKeyAliases.clearSelection(); + } + + // reset the password + mKeyPassword.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } catch (KeyStoreException e) { + onException(e); + } catch (FileNotFoundException e) { + onException(e); + } catch (NoSuchAlgorithmException e) { + onException(e); + } catch (CertificateException e) { + onException(e); + } catch (IOException e) { + onException(e); + } finally { + // in case we exit with an exception, we need to reset this + mDisableOnChange = false; + } + } + } + + @Override + public IWizardPage getPreviousPage() { + return mWizard.getKeystoreSelectionPage(); + } + + @Override + public IWizardPage getNextPage() { + if (mWizard.getKeyCreationMode()) { + return mWizard.getKeyCreationPage(); + } + + return mWizard.getKeyCheckPage(); + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + if (mWizard.getKeyCreationMode() == false) { + if (mKeyAliases.getSelectionIndex() == -1) { + setErrorMessage("Select a key alias."); + setPageComplete(false); + return; + } + + if (mKeyPassword.getText().trim().length() == 0) { + setErrorMessage("Enter key password."); + setPageComplete(false); + return; + } + } + + setPageComplete(true); + } + + private void enableWidgets() { + boolean useKey = !mWizard.getKeyCreationMode(); + mKeyAliasesLabel.setEnabled(useKey); + mKeyAliases.setEnabled(useKey); + mKeyPassword.setEnabled(useKey); + mKeyPasswordLabel.setEnabled(useKey); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java new file mode 100644 index 000000000..eabee15a2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeystoreSelectionPage.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * Keystore selection page. This page allows to choose to create a new keystore or use an + * existing one. + */ +final class KeystoreSelectionPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Button mUseExistingKeystore; + private Button mCreateKeystore; + private Text mKeystore; + private Text mKeystorePassword; + private Label mConfirmLabel; + private Text mKeystorePassword2; + private boolean mDisableOnChange = false; + + protected KeystoreSelectionPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Keystore selection"); + setDescription(""); //TODO + } + + @Override + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + composite.setLayout(gl); + + GridData gd; + + mUseExistingKeystore = new Button(composite, SWT.RADIO); + mUseExistingKeystore.setText("Use existing keystore"); + mUseExistingKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + mUseExistingKeystore.setSelection(true); + + mCreateKeystore = new Button(composite, SWT.RADIO); + mCreateKeystore.setText("Create new keystore"); + mCreateKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + + new Label(composite, SWT.NONE).setText("Location:"); + mKeystore = new Text(composite, SWT.BORDER); + mKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + final Button browseButton = new Button(composite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog; + if (mUseExistingKeystore.getSelection()) { + fileDialog = new FileDialog(browseButton.getShell(),SWT.OPEN); + fileDialog.setText("Load Keystore"); + } else { + fileDialog = new FileDialog(browseButton.getShell(),SWT.SAVE); + fileDialog.setText("Select Keystore Name"); + } + + String fileName = fileDialog.open(); + if (fileName != null) { + mKeystore.setText(fileName); + } + } + }); + + new Label(composite, SWT.NONE).setText("Password:"); + mKeystorePassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeystorePassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeystorePassword.addVerifyListener(sPasswordVerifier); + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mConfirmLabel = new Label(composite, SWT.NONE); + mConfirmLabel.setText("Confirm:"); + mKeystorePassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeystorePassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeystorePassword2.addVerifyListener(sPasswordVerifier); + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + mKeystorePassword2.setEnabled(false); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mUseExistingKeystore.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean createStore = !mUseExistingKeystore.getSelection(); + mKeystorePassword2.setEnabled(createStore); + mConfirmLabel.setEnabled(createStore); + mWizard.setKeystoreCreationMode(createStore); + onChange(); + } + }); + + mKeystore.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mWizard.setKeystore(mKeystore.getText().trim()); + onChange(); + } + }); + + mKeystorePassword.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mWizard.setKeystorePassword(mKeystorePassword.getText()); + onChange(); + } + }); + + mKeystorePassword2.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + onChange(); + } + }); + } + + @Override + public IWizardPage getNextPage() { + if (mUseExistingKeystore.getSelection()) { + return mWizard.getKeySelectionPage(); + } + + return mWizard.getKeyCreationPage(); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & DATA_PROJECT) != 0) { + // reset the keystore/alias from the content of the project + IProject project = mWizard.getProject(); + + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + String keystore = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_KEYSTORE); + if (keystore != null) { + mKeystore.setText(keystore); + } + + // reset the passwords + mKeystorePassword.setText(""); //$NON-NLS-1$ + mKeystorePassword2.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + boolean createStore = !mUseExistingKeystore.getSelection(); + + // checks the keystore path is non null. + String keystore = mKeystore.getText().trim(); + if (keystore.length() == 0) { + setErrorMessage("Enter path to keystore."); + setPageComplete(false); + return; + } else { + File f = new File(keystore); + if (f.exists() == false) { + if (createStore == false) { + setErrorMessage("Keystore does not exist."); + setPageComplete(false); + return; + } + } else if (f.isDirectory()) { + setErrorMessage("Keystore path is a directory."); + setPageComplete(false); + return; + } else if (f.isFile()) { + if (createStore) { + setErrorMessage("File already exists."); + setPageComplete(false); + return; + } + } + } + + String value = mKeystorePassword.getText(); + if (value.length() == 0) { + setErrorMessage("Enter keystore password."); + setPageComplete(false); + return; + } else if (createStore && value.length() < 6) { + setErrorMessage("Keystore password is too short - must be at least 6 characters."); + setPageComplete(false); + return; + } + + if (createStore) { + if (mKeystorePassword2.getText().length() == 0) { + setErrorMessage("Confirm keystore password."); + setPageComplete(false); + return; + } + + if (mKeystorePassword.getText().equals(mKeystorePassword2.getText()) == false) { + setErrorMessage("Keystore passwords do not match."); + setPageComplete(false); + return; + } + } + + setPageComplete(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java new file mode 100644 index 000000000..b8a7043da --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ProjectCheckPage.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2008 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.export; + +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +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.NonLibraryProjectOnlyFilter; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.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.Text; + +/** + * First Export Wizard Page. Display warning/errors. + */ +final class ProjectCheckPage extends ExportWizardPage { + private final static String IMG_ERROR = "error.png"; //$NON-NLS-1$ + private final static String IMG_WARNING = "warning.png"; //$NON-NLS-1$ + + private final ExportWizard mWizard; + private Image mError; + private Image mWarning; + private boolean mHasMessage = false; + private Composite mTopComposite; + private Composite mErrorComposite; + private Text mProjectText; + private ProjectChooserHelper mProjectChooserHelper; + private boolean mFirstOnShow = true; + + protected ProjectCheckPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Project Checks"); + setDescription("Performs a set of checks to make sure the application can be exported."); + } + + @Override + public void createControl(Composite parent) { + mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), + new NonLibraryProjectOnlyFilter()); + + GridLayout gl = null; + GridData gd = null; + + mTopComposite = new Composite(parent, SWT.NONE); + mTopComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mTopComposite.setLayout(new GridLayout(1, false)); + + // composite for the project selection. + Composite projectComposite = new Composite(mTopComposite, SWT.NONE); + projectComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + projectComposite.setLayout(gl = new GridLayout(3, false)); + gl.marginHeight = gl.marginWidth = 0; + + Label label = new Label(projectComposite, SWT.NONE); + label.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + label.setText("Select the project to export:"); + + new Label(projectComposite, SWT.NONE).setText("Project:"); + mProjectText = new Text(projectComposite, SWT.BORDER); + mProjectText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mProjectText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + handleProjectNameChange(); + } + }); + + Button browseButton = new Button(projectComposite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject( + mProjectText.getText().trim(), + "Please select a project to export"); + + if (javaProject != null) { + IProject project = javaProject.getProject(); + + // set the new name in the text field. The modify listener will take + // care of updating the status and the ExportWizard object. + mProjectText.setText(project.getName()); + } + } + }); + + setControl(mTopComposite); + } + + @Override + void onShow() { + if (mFirstOnShow) { + // get the project and init the ui + IProject project = mWizard.getProject(); + if (project != null) { + mProjectText.setText(project.getName()); + } + + mFirstOnShow = false; + } + } + + private void buildErrorUi(IProject project) { + // Show description the first time + setErrorMessage(null); + setMessage(null); + setPageComplete(true); + mHasMessage = false; + + // composite parent for the warning/error + GridLayout gl = null; + mErrorComposite = new Composite(mTopComposite, SWT.NONE); + mErrorComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing *= 3; // more spacing than normal. + mErrorComposite.setLayout(gl); + + if (project == null) { + setErrorMessage("Select project to export."); + mHasMessage = true; + } else { + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + addError(mErrorComposite, "Project is not an Android project."); + } else { + // check for errors + if (ProjectHelper.hasError(project, true)) { + addError(mErrorComposite, "Project has compilation error(s)"); + } + + // check the project output + IFolder outputIFolder = BaseProjectHelper.getJavaOutputFolder(project); + if (outputIFolder == null) { + addError(mErrorComposite, + "Unable to get the output folder of the project!"); + } + + // project is an android project, we check the debuggable attribute. + ManifestData manifestData = AndroidManifestHelper.parseForData(project); + Boolean debuggable = null; + if (manifestData != null) { + debuggable = manifestData.getDebuggable(); + } + + if (debuggable != null && debuggable == Boolean.TRUE) { + addWarning(mErrorComposite, + "The manifest 'debuggable' attribute is set to true.\n" + + "You should set it to false for applications that you release to the public.\n\n" + + "Applications with debuggable=true are compiled in debug mode always."); + } + + // check for mapview stuff + } + } catch (CoreException e) { + // unable to access nature + addError(mErrorComposite, "Unable to get project nature"); + } + } + + if (mHasMessage == false) { + Label label = new Label(mErrorComposite, SWT.NONE); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + label.setLayoutData(gd); + label.setText("No errors found. Click Next."); + } + + mTopComposite.layout(); + } + + /** + * Adds an error label to a {@link Composite} object. + * @param parent the Composite parent. + * @param message the error message. + */ + private void addError(Composite parent, String message) { + if (mError == null) { + mError = IconFactory.getInstance().getIcon(IMG_ERROR); + } + + new Label(parent, SWT.NONE).setImage(mError); + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + label.setText(message); + + setErrorMessage("Application cannot be exported due to the error(s) below."); + setPageComplete(false); + mHasMessage = true; + } + + /** + * Adds a warning label to a {@link Composite} object. + * @param parent the Composite parent. + * @param message the warning message. + */ + private void addWarning(Composite parent, String message) { + if (mWarning == null) { + mWarning = IconFactory.getInstance().getIcon(IMG_WARNING); + } + + new Label(parent, SWT.NONE).setImage(mWarning); + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + label.setText(message); + + mHasMessage = true; + } + + /** + * Checks the parameters for correctness, and update the error message and buttons. + */ + private void handleProjectNameChange() { + setPageComplete(false); + + if (mErrorComposite != null) { + mErrorComposite.dispose(); + mErrorComposite = null; + } + + // update the wizard with the new project + mWizard.setProject(null); + + //test the project name first! + String text = mProjectText.getText().trim(); + if (text.length() == 0) { + setErrorMessage("Select project to export."); + } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) { + setErrorMessage("Project name contains unsupported characters!"); + } else { + IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null); + IProject found = null; + for (IJavaProject javaProject : projects) { + if (javaProject.getProject().getName().equals(text)) { + found = javaProject.getProject(); + break; + } + + } + + if (found != null) { + setErrorMessage(null); + + // update the wizard with the new project + mWizard.setProject(found); + + // now rebuild the error ui. + buildErrorUi(found); + } else { + setErrorMessage(String.format("There is no android project named '%1$s'", + text)); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java new file mode 100644 index 000000000..d3df0584f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import static com.android.SdkConstants.GRADLE_LATEST_VERSION; +import static com.android.SdkConstants.GRADLE_PLUGIN_LATEST_VERSION; +import static com.android.SdkConstants.GRADLE_PLUGIN_NAME; +import static com.android.tools.lint.checks.GradleDetector.APP_PLUGIN_ID; +import static com.android.tools.lint.checks.GradleDetector.LIB_PLUGIN_ID; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFile; +import com.android.sdklib.io.FileOp; +import com.android.xml.AndroidManifest; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.google.common.io.Closeables; +import com.google.common.io.Files; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Shell; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +/** + * Creates build.gradle and settings.gradle files for a set of projects. + * <p> + * Based on {@link org.eclipse.ant.internal.ui.datatransfer.BuildFileCreator} + */ +public class BuildFileCreator { + static final String BUILD_FILE = "build.gradle"; //$NON-NLS-1$ + static final String SETTINGS_FILE = "settings.gradle"; //$NON-NLS-1$ + private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$ + private static final String GRADLE_WRAPPER_LOCATION = + "tools/templates/gradle/wrapper"; //$NON-NLS-1$ + static final String PLUGIN_CLASSPATH = + "classpath '" + GRADLE_PLUGIN_NAME + GRADLE_PLUGIN_LATEST_VERSION + "'"; //$NON-NLS-1$ + static final String MAVEN_REPOSITORY = "jcenter()"; //$NON-NLS-1$ + + private static final String[] GRADLE_WRAPPER_FILES = new String[] { + "gradlew", //$NON-NLS-1$ + "gradlew.bat", //$NON-NLS-1$ + "gradle/wrapper/gradle-wrapper.jar", //$NON-NLS-1$ + "gradle/wrapper/gradle-wrapper.properties" //$NON-NLS-1$ + }; + + private static final Comparator<IFile> FILE_COMPARATOR = new Comparator<IFile>() { + @Override + public int compare(IFile o1, IFile o2) { + return o1.toString().compareTo(o2.toString()); + } + }; + + private final GradleModule mModule; + private final StringBuilder mBuildFile = new StringBuilder(); + + /** + * Create buildfile for the projects. + * + * @param shell parent instance for dialogs + * @return project names for which buildfiles were created + * @throws InterruptedException thrown when user cancels task + */ + public static void createBuildFiles( + @NonNull ProjectSetupBuilder builder, + @NonNull Shell shell, + @NonNull IProgressMonitor pm) { + + File gradleLocation = new File(Sdk.getCurrent().getSdkOsLocation(), GRADLE_WRAPPER_LOCATION); + SubMonitor localmonitor = null; + + try { + // See if we have a Gradle wrapper in the SDK templates directory. If so, we can copy + // it over. + boolean hasGradleWrapper = true; + for (File wrapperFile : getGradleWrapperFiles(gradleLocation)) { + if (!wrapperFile.exists()) { + hasGradleWrapper = false; + } + } + + Collection<GradleModule> modules = builder.getModules(); + boolean multiModules = modules.size() > 1; + + // determine files to create/change + List<IFile> files = new ArrayList<IFile>(); + + // add the build.gradle file for all modules. + for (GradleModule module : modules) { + // build.gradle file + IFile file = module.getProject().getFile(BuildFileCreator.BUILD_FILE); + files.add(file); + } + + // get the commonRoot for all modules. If only one module, this returns the path + // of the project. + IPath commonRoot = builder.getCommonRoot(); + + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IPath workspaceLocation = workspaceRoot.getLocation(); + + IPath relativePath = commonRoot.makeRelativeTo(workspaceLocation); + // if makeRelativePath to returns the same path, then commonRoot is not in the + // workspace. + boolean rootInWorkspace = !relativePath.equals(commonRoot); + // we only care if the root is a workspace project. if it's the workspace folder itself, + // then the files won't be handled by the workspace. + rootInWorkspace = rootInWorkspace && relativePath.segmentCount() > 0; + + File settingsFile = new File(commonRoot.toFile(), SETTINGS_FILE); + + // more than one modules -> generate settings.gradle + if (multiModules && rootInWorkspace) { + + // Locate the settings.gradle file and add it to the changed files list + IPath settingsGradle = Path.fromOSString(settingsFile.getAbsolutePath()); + + // different path, means commonRoot is inside the workspace, which means we have + // to add settings.gradle and wrapper files to the list of files to add. + IFile iFile = workspaceRoot.getFile(settingsGradle); + if (iFile != null) { + files.add(iFile); + } + } + + // Gradle wrapper files + if (hasGradleWrapper && rootInWorkspace) { + // See if there already wrapper files there and only mark nonexistent ones for + // creation. + for (File wrapperFile : getGradleWrapperFiles(commonRoot.toFile())) { + if (!wrapperFile.exists()) { + IPath path = Path.fromOSString(wrapperFile.getAbsolutePath()); + IFile file = workspaceRoot.getFile(path); + files.add(file); + } + } + } + + ExportStatus status = new ExportStatus(); + builder.setStatus(status); + + // Trigger checkout of changed files + Set<IFile> confirmedFiles = validateEdit(files, status, shell); + + if (status.hasError()) { + return; + } + + // Now iterate over all the modules and generate the build files. + localmonitor = SubMonitor.convert(pm, ExportMessages.PageTitle, + confirmedFiles.size()); + List<String> projectSettingsPath = Lists.newArrayList(); + for (GradleModule currentModule : modules) { + IProject moduleProject = currentModule.getProject(); + + IFile file = moduleProject.getFile(BuildFileCreator.BUILD_FILE); + if (!confirmedFiles.contains(file)) { + continue; + } + + localmonitor.setTaskName(NLS.bind(ExportMessages.FileStatusMessage, + moduleProject.getName())); + + ProjectState projectState = Sdk.getProjectState(moduleProject); + BuildFileCreator instance = new BuildFileCreator(currentModule, shell); + if (projectState != null) { + // This is an Android project + if (!multiModules) { + instance.appendBuildScript(); + } + instance.appendHeader(projectState.isLibrary()); + instance.appendDependencies(); + instance.startAndroidTask(projectState); + //instance.appendDefaultConfig(); + instance.createAndroidSourceSets(); + instance.finishAndroidTask(); + } else { + // This is a plain Java project + instance.appendJavaHeader(); + instance.createJavaSourceSets(); + } + + try { + // Write the build file + String buildfile = instance.mBuildFile.toString(); + InputStream is = + new ByteArrayInputStream(buildfile.getBytes("UTF-8")); //$NON-NLS-1$ + if (file.exists()) { + file.setContents(is, true, true, null); + } else { + file.create(is, true, null); + } + } catch (Exception e) { + status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, + file.getLocation().toFile()); + status.setErrorMessage(e.getMessage()); + return; + } + + if (localmonitor.isCanceled()) { + return; + } + localmonitor.worked(1); + + // get the project path to add it to the settings.gradle. + projectSettingsPath.add(currentModule.getPath()); + } + + // write the settings file. + if (multiModules) { + try { + writeGradleSettingsFile(settingsFile, projectSettingsPath); + } catch (IOException e) { + status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, settingsFile); + status.setErrorMessage(e.getMessage()); + return; + } + File mainBuildFile = new File(commonRoot.toFile(), BUILD_FILE); + try { + writeRootBuildGradle(mainBuildFile); + } catch (IOException e) { + status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, mainBuildFile); + status.setErrorMessage(e.getMessage()); + return; + } + } + + // finally write the wrapper + // TODO check we can based on where it is + if (hasGradleWrapper) { + copyGradleWrapper(gradleLocation, commonRoot.toFile(), status); + if (status.hasError()) { + return; + } + } + + } finally { + if (localmonitor != null && !localmonitor.isCanceled()) { + localmonitor.done(); + } + if (pm != null) { + pm.done(); + } + } + } + + /** + * @param GradleModule create buildfile for this project + * @param shell parent instance for dialogs + */ + private BuildFileCreator(GradleModule module, Shell shell) { + mModule = module; + } + + /** + * Return the files that comprise the Gradle wrapper as a collection of {@link File} instances. + * @param root + * @return + */ + private static List<File> getGradleWrapperFiles(File root) { + List<File> files = new ArrayList<File>(GRADLE_WRAPPER_FILES.length); + for (String file : GRADLE_WRAPPER_FILES) { + files.add(new File(root, file)); + } + return files; + } + + /** + * Copy the Gradle wrapper files from one directory to another. + */ + private static void copyGradleWrapper(File from, File to, ExportStatus status) { + for (String file : GRADLE_WRAPPER_FILES) { + File dest = new File(to, file); + try { + File src = new File(from, file); + dest.getParentFile().mkdirs(); + new FileOp().copyFile(src, dest); + + if (src.getName().equals(GRADLE_PROPERTIES)) { + updateGradleDistributionUrl(GRADLE_LATEST_VERSION, dest); + } + dest.setExecutable(src.canExecute()); + status.addFileStatus(ExportStatus.FileStatus.OK, dest); + } catch (IOException e) { + status.addFileStatus(ExportStatus.FileStatus.IO_FAILURE, dest); + return; + } + } + } + + /** + * Outputs boilerplate buildscript information common to all Gradle build files. + */ + private void appendBuildScript() { + appendBuildScript(mBuildFile); + } + + /** + * Outputs boilerplate header information common to all Gradle build files. + */ + private static void appendBuildScript(StringBuilder builder) { + builder.append("buildscript {\n"); //$NON-NLS-1$ + builder.append(" repositories {\n"); //$NON-NLS-1$ + builder.append(" " + MAVEN_REPOSITORY + "\n"); //$NON-NLS-1$ + builder.append(" }\n"); //$NON-NLS-1$ + builder.append(" dependencies {\n"); //$NON-NLS-1$ + builder.append(" " + PLUGIN_CLASSPATH + "\n"); //$NON-NLS-1$ + builder.append(" }\n"); //$NON-NLS-1$ + builder.append("}\n"); //$NON-NLS-1$ + } + + /** + * Outputs boilerplate header information common to all Gradle build files. + */ + private void appendHeader(boolean isLibrary) { + if (isLibrary) { + mBuildFile.append("apply plugin: '").append(LIB_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + mBuildFile.append("apply plugin: '").append(APP_PLUGIN_ID).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + mBuildFile.append("\n"); //$NON-NLS-1$ + } + + /** + * Outputs a block which sets up library and project dependencies. + */ + private void appendDependencies() { + mBuildFile.append("dependencies {\n"); //$NON-NLS-1$ + + // first the local jars. + // TODO: Fix + mBuildFile.append(" compile fileTree(dir: 'libs', include: '*.jar')\n"); //$NON-NLS-1$ + + for (GradleModule dep : mModule.getDependencies()) { + mBuildFile.append(" compile project('" + dep.getPath() + "')\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + mBuildFile.append("}\n"); //$NON-NLS-1$ + mBuildFile.append("\n"); //$NON-NLS-1$ + } + + /** + * Outputs the beginning of an Android task in the build file. + */ + private void startAndroidTask(ProjectState projectState) { + int buildApi = projectState.getTarget().getVersion().getApiLevel(); + String toolsVersion = projectState.getTarget().getBuildToolInfo().getRevision().toString(); + mBuildFile.append("android {\n"); //$NON-NLS-1$ + mBuildFile.append(" compileSdkVersion " + buildApi + "\n"); //$NON-NLS-1$ + mBuildFile.append(" buildToolsVersion \"" + toolsVersion + "\"\n"); //$NON-NLS-1$ + mBuildFile.append("\n"); //$NON-NLS-1$ + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(projectState.getProject()); + // otherwise we check source compatibility + String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true); + if (JavaCore.VERSION_1_7.equals(source)) { + mBuildFile.append( + " compileOptions {\n" + //$NON-NLS-1$ + " sourceCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$ + " targetCompatibility JavaVersion.VERSION_1_7\n" + //$NON-NLS-1$ + " }\n" + //$NON-NLS-1$ + "\n"); //$NON-NLS-1$ + } + } catch (CoreException e) { + // Ignore compliance level, go with default + } + } + + /** + * Outputs a sourceSets block to the Android task that locates all of the various source + * subdirectories in the project. + */ + private void createAndroidSourceSets() { + IFolderWrapper projectFolder = new IFolderWrapper(mModule.getProject()); + IAbstractFile mManifestFile = AndroidManifest.getManifest(projectFolder); + if (mManifestFile == null) { + return; + } + List<String> srcDirs = new ArrayList<String>(); + for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE || + SdkConstants.FD_GEN_SOURCES.equals(entry.getPath().lastSegment())) { + continue; + } + IPath path = entry.getPath().removeFirstSegments(1); + srcDirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$ + } + + String srcPaths = Joiner.on(",").join(srcDirs); + + mBuildFile.append(" sourceSets {\n"); //$NON-NLS-1$ + mBuildFile.append(" main {\n"); //$NON-NLS-1$ + mBuildFile.append(" manifest.srcFile '" + SdkConstants.FN_ANDROID_MANIFEST_XML + "'\n"); //$NON-NLS-1$ + mBuildFile.append(" java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" aidl.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" renderscript.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" res.srcDirs = ['res']\n"); //$NON-NLS-1$ + mBuildFile.append(" assets.srcDirs = ['assets']\n"); //$NON-NLS-1$ + mBuildFile.append(" }\n"); //$NON-NLS-1$ + mBuildFile.append("\n"); //$NON-NLS-1$ + mBuildFile.append(" // Move the tests to tests/java, tests/res, etc...\n"); //$NON-NLS-1$ + mBuildFile.append(" instrumentTest.setRoot('tests')\n"); //$NON-NLS-1$ + if (srcDirs.contains("'src'")) { + mBuildFile.append("\n"); //$NON-NLS-1$ + mBuildFile.append(" // Move the build types to build-types/<type>\n"); //$NON-NLS-1$ + mBuildFile.append(" // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...\n"); //$NON-NLS-1$ + mBuildFile.append(" // This moves them out of them default location under src/<type>/... which would\n"); //$NON-NLS-1$ + mBuildFile.append(" // conflict with src/ being used by the main source set.\n"); //$NON-NLS-1$ + mBuildFile.append(" // Adding new build types or product flavors should be accompanied\n"); //$NON-NLS-1$ + mBuildFile.append(" // by a similar customization.\n"); //$NON-NLS-1$ + mBuildFile.append(" debug.setRoot('build-types/debug')\n"); //$NON-NLS-1$ + mBuildFile.append(" release.setRoot('build-types/release')\n"); //$NON-NLS-1$ + } + mBuildFile.append(" }\n"); //$NON-NLS-1$ + } + + /** + * Outputs the completion of the Android task in the build file. + */ + private void finishAndroidTask() { + mBuildFile.append("}\n"); //$NON-NLS-1$ + } + + /** + * Outputs a boilerplate header for non-Android projects + */ + private void appendJavaHeader() { + mBuildFile.append("apply plugin: 'java'\n"); //$NON-NLS-1$ + } + + /** + * Outputs a sourceSets block for non-Android projects to locate the source directories. + */ + private void createJavaSourceSets() { + List<String> dirs = new ArrayList<String>(); + for (IClasspathEntry entry : mModule.getJavaProject().readRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) { + continue; + } + IPath path = entry.getPath().removeFirstSegments(1); + dirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$ + } + + String srcPaths = Joiner.on(",").join(dirs); + + mBuildFile.append("sourceSets {\n"); //$NON-NLS-1$ + mBuildFile.append(" main.java.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" main.resources.srcDirs = [" + srcPaths + "]\n"); //$NON-NLS-1$ + mBuildFile.append(" test.java.srcDirs = ['tests/java']\n"); //$NON-NLS-1$ + mBuildFile.append(" test.resources.srcDirs = ['tests/resources']\n"); //$NON-NLS-1$ + mBuildFile.append("}\n"); //$NON-NLS-1$ + } + + /** + * Merges the new subproject dependencies into the settings.gradle file if it already exists, + * and creates one if it does not. + * @throws IOException + */ + private static void writeGradleSettingsFile(File settingsFile, List<String> projectPaths) + throws IOException { + StringBuilder contents = new StringBuilder(); + for (String path : projectPaths) { + contents.append("include '").append(path).append("'\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + Files.write(contents.toString(), settingsFile, Charsets.UTF_8); + } + + private static void writeRootBuildGradle(File buildFile) throws IOException { + StringBuilder sb = new StringBuilder( + "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n"); + + appendBuildScript(sb); + + Files.write(sb.toString(), buildFile, Charsets.UTF_8); + } + + /** + * Request write access to given files. Depending on the version control + * plug-in opens a confirm checkout dialog. + * + * @param shell + * parent instance for dialogs + * @return <code>IFile</code> objects for which user confirmed checkout + * @throws CoreException + * thrown if project is under version control, but not connected + */ + static Set<IFile> validateEdit( + @NonNull List<IFile> files, + @NonNull ExportStatus exportStatus, + @NonNull Shell shell) { + Set<IFile> confirmedFiles = new TreeSet<IFile>(FILE_COMPARATOR); + if (files.size() == 0) { + return confirmedFiles; + } + IStatus status = (files.get(0)).getWorkspace().validateEdit( + files.toArray(new IFile[files.size()]), shell); + if (status.isMultiStatus() && status.getChildren().length > 0) { + for (int i = 0; i < status.getChildren().length; i++) { + IStatus statusChild = status.getChildren()[i]; + if (statusChild.isOK()) { + confirmedFiles.add(files.get(i)); + } else { + exportStatus.addFileStatus( + ExportStatus.FileStatus.VCS_FAILURE, + files.get(i).getLocation().toFile()); + } + } + } else if (status.isOK()) { + confirmedFiles.addAll(files); + } + if (status.getSeverity() == IStatus.ERROR) { + // not possible to checkout files: not connected to version + // control plugin or hijacked files and made read-only, so + // collect error messages provided by validator and re-throw + StringBuffer message = new StringBuffer(status.getPlugin() + ": " //$NON-NLS-1$ + + status.getMessage() + NEWLINE); + if (status.isMultiStatus()) { + for (int i = 0; i < status.getChildren().length; i++) { + IStatus statusChild = status.getChildren()[i]; + message.append(statusChild.getMessage() + NEWLINE); + } + } + String s = message.toString(); + exportStatus.setErrorMessage(s); + } + + return confirmedFiles; + } + + // ------------------------------------------------------------------------------- + // Fix gradle wrapper version. This code is from GradleUtil in the Studio plugin: + // ------------------------------------------------------------------------------- + + private static final String GRADLE_PROPERTIES = "gradle-wrapper.properties"; + private static final String GRADLEW_PROPERTIES_PATH = + "gradle" + File.separator + "wrapper" + File.separator + GRADLE_PROPERTIES; + private static final String GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME = "distributionUrl"; + + @NonNull + private static File getGradleWrapperPropertiesFilePath(@NonNull File projectRootDir) { + return new File(projectRootDir, GRADLEW_PROPERTIES_PATH); + } + + @Nullable + public static File findWrapperPropertiesFile(@NonNull File projectRootDir) { + File wrapperPropertiesFile = getGradleWrapperPropertiesFilePath(projectRootDir); + return wrapperPropertiesFile.isFile() ? wrapperPropertiesFile : null; + } + + private static boolean updateGradleDistributionUrl( + @NonNull String gradleVersion, + @NonNull File propertiesFile) throws IOException { + Properties properties = loadGradleWrapperProperties(propertiesFile); + String gradleDistributionUrl = getGradleDistributionUrl(gradleVersion, false); + String property = properties.getProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME); + if (property != null + && (property.equals(gradleDistributionUrl) || property + .equals(getGradleDistributionUrl(gradleVersion, true)))) { + return false; + } + properties.setProperty(GRADLEW_DISTRIBUTION_URL_PROPERTY_NAME, gradleDistributionUrl); + FileOutputStream out = null; + try { + out = new FileOutputStream(propertiesFile); + properties.store(out, null); + return true; + } finally { + Closeables.close(out, true); + } + } + + @NonNull + private static Properties loadGradleWrapperProperties(@NonNull File propertiesFile) + throws IOException { + Properties properties = new Properties(); + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(propertiesFile); + properties.load(fileInputStream); + return properties; + } finally { + Closeables.close(fileInputStream, true); + } + } + + @NonNull + private static String getGradleDistributionUrl(@NonNull String gradleVersion, + boolean binOnly) { + String suffix = binOnly ? "bin" : "all"; + return String.format("https://services.gradle.org/distributions/gradle-%1$s-" + suffix + + ".zip", gradleVersion); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java new file mode 100644 index 000000000..1f236fb2b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ConfirmationPage.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.google.common.collect.Lists; + +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableLayout; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.ui.model.WorkbenchLabelProvider; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +/** + * Confirmation page to review the actual project export + * list and see warning about existing files. + * + */ +public class ConfirmationPage extends WizardPage { + + private final ProjectSetupBuilder mBuilder; + private TableViewer mTableViewer; + private Label mModuleDescription1; + private Label mModuleDescription2; + private Label mModuleDescription3; + private Label mProjectRootLabel; + private Label mProjectRootWarning; + private List<IJavaProject> mOverrideProjects; + private boolean mOverrideWarning; + private Button mForceOverride; + + public ConfirmationPage(ProjectSetupBuilder builder) { + super("ConfirmationPage"); //$NON-NLS-1$ + mBuilder = builder; + setPageComplete(false); + setTitle(ExportMessages.PageTitle); + setDescription(ExportMessages.PageDescription); + } + + @Override + public void createControl(Composite parent) { + initializeDialogUnits(parent); + GridData data; + + Composite workArea = new Composite(parent, SWT.NONE); + setControl(workArea); + + workArea.setLayout(new GridLayout()); + workArea.setLayoutData(new GridData(GridData.FILL_BOTH + | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL)); + + Label title = new Label(workArea, SWT.NONE); + title.setText("Please review the export options."); + + Group group = new Group(workArea, SWT.NONE); + group.setText("Project root"); + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + + mProjectRootLabel = new Label(group, SWT.NONE); + mProjectRootLabel.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + + mProjectRootWarning = new Label(group, SWT.NONE); + mProjectRootWarning.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + + Group group2 = new Group(workArea, SWT.NONE); + group2.setText("Exported Modules"); + group2.setLayout(new GridLayout()); + group2.setLayoutData(data = new GridData(SWT.FILL, SWT.FILL, true, true)); + data.heightHint = 300; + + Table table = new Table(group2, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + mTableViewer = new TableViewer(table); + table.setLayout(new TableLayout()); + table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + mTableViewer.setContentProvider(new IStructuredContentProvider() { + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof ProjectSetupBuilder) { + ProjectSetupBuilder builder = (ProjectSetupBuilder) inputElement; + Collection<GradleModule> modules = builder.getModules(); + Object[] array = new Object[modules.size()]; + int i = 0; + for (GradleModule module : modules) { + array[i++] = module.getJavaProject(); + } + + return array; + } + + return null; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + }); + mTableViewer.setLabelProvider(new WorkbenchLabelProvider() { + @Override + protected String decorateText(String input, Object element) { + if (element instanceof IJavaProject) { + IJavaProject javaProject = (IJavaProject) element; + StringBuilder sb = new StringBuilder(input); + if (!mBuilder.isOriginalProject(javaProject)) { + sb.append('*'); + } + // TODO: decorate icon instead? + if (mOverrideProjects.contains(javaProject)) { + sb.append(" (1 warning)"); + } + + return sb.toString(); + } + + return input; + } + }); + mTableViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + Object firstElement = selection.getFirstElement(); + if (firstElement instanceof IJavaProject) { + GradleModule module = mBuilder.getModule((IJavaProject) firstElement); + if (mBuilder.getOriginalModules().contains(module)) { + mModuleDescription1.setText("Exported because selected in previous page."); + } else { + List<GradleModule> list = mBuilder.getShortestDependencyTo(module); + StringBuilder sb = new StringBuilder(); + for (GradleModule m : list) { + if (sb.length() > 0) { + sb.append(" > "); + } + sb.append(m.getJavaProject().getProject().getName()); + } + mModuleDescription1.setText("Dependency chain: " + sb); + } + mModuleDescription2.setText("Path: " + module.getPath()); + + if (mOverrideProjects.contains(module.getJavaProject())) { + mModuleDescription3.setText( + "WARNING: build.gradle already exists for this project"); + } else { + mModuleDescription3.setText(""); + } + } else { + mModuleDescription1.setText(""); + mModuleDescription2.setText(""); + mModuleDescription3.setText(""); + } + } + }); + + mModuleDescription1 = new Label(group2, SWT.NONE); + mModuleDescription1.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + mModuleDescription2 = new Label(group2, SWT.NONE); + mModuleDescription2.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + mModuleDescription3 = new Label(group2, SWT.NONE); + mModuleDescription3.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + + mForceOverride = new Button(workArea, SWT.CHECK); + mForceOverride.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); + mForceOverride.setText("Force overriding of existing files"); + mForceOverride.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateEnablement(); + } + }); + + setControl(workArea); + Dialog.applyDialogFont(parent); + } + + /** + * Get list of projects which have already a buildfile. + * + * @param javaProjects list of IJavaProject objects + * @return set of project names + */ + private void computeOverride(String commonRoot) { + mOverrideProjects = Lists.newArrayList(); + for (GradleModule module : mBuilder.getModules()) { + if (new File(module.getProject().getLocation().toFile(), + BuildFileCreator.BUILD_FILE).exists()) { + mOverrideProjects.add(module.getJavaProject()); + } + } + + // also check on the root settings.gradle/build.gradle + boolean settingsFile = new File(commonRoot, BuildFileCreator.SETTINGS_FILE).exists(); + boolean buildFile = new File(commonRoot, BuildFileCreator.BUILD_FILE).exists(); + if (settingsFile && buildFile) { + mProjectRootWarning.setText( + "WARNING: build.gradle/settings.gradle already exists at this location."); + } else if (settingsFile) { + mProjectRootWarning.setText( + "WARNING: settings.gradle already exists at this location."); + } else if (buildFile) { + mProjectRootWarning.setText("WARNING: build.gradle already exists at this location."); + } + + mOverrideWarning = mOverrideProjects.size() > 0 || settingsFile || buildFile; + } + + /** + * Enables/disables the finish button on the wizard and displays error messages as needed. + */ + private void updateEnablement() { + if (mOverrideWarning && !mForceOverride.getSelection()) { + setErrorMessage("Enable overriding of existing files before clicking Finish"); + mBuilder.setCanGenerate(false); + } else { + setErrorMessage(null); + mBuilder.setCanGenerate(true); + } + setPageComplete(false); + getContainer().updateButtons(); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + mProjectRootWarning.setText(""); + + String commonRoot = mBuilder.getCommonRoot().toOSString(); + computeOverride(commonRoot); + mProjectRootLabel.setText(commonRoot); + mTableViewer.setInput(mBuilder); + mTableViewer.getTable().setFocus(); + mBuilder.setCanFinish(false); + mBuilder.setCanGenerate(true); + updateEnablement(); + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java new file mode 100644 index 000000000..c7d6c1748 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import org.eclipse.osgi.util.NLS; + +public class ExportMessages extends NLS { + private static final String BUNDLE_NAME = + "com.android.ide.eclipse.adt.internal.wizards.exportgradle.ExportMessages";//$NON-NLS-1$ + + public static String PageTitle; + public static String PageDescription; + public static String SelectProjects; + public static String ConfirmOverwrite; + public static String ConfirmOverwriteTitle; + public static String CyclicProjectsError; + public static String ExportFailedError; + public static String SelectAll; + public static String DeselectAll; + public static String NoProjectsError; + public static String StatusMessage; + public static String FileStatusMessage; + public static String WindowTitle; + + static { + // load message values from bundle file + NLS.initializeMessages(BUNDLE_NAME, ExportMessages.class); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties new file mode 100644 index 000000000..1a6dbb192 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties @@ -0,0 +1,27 @@ +# Copyright (C) 2013 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. + +PageTitle=Generate Gradle Build files +PageDescription=Generates Gradle build files based on the configuration of the Java projects +SelectProjects=Select the projects to use to &generate the Gradle buildfiles: +ConfirmOverwrite=Are you sure you want to overwrite the buildfiles for these projects? +ConfirmOverwriteTitle=Overwrite Buildfiles? +CyclicProjectsError=A cycle was detected in the build path of project: {0} +ExportFailedError=Buildfile export failed: {0}. See the error log for more details. +SelectAll=&Select All +DeselectAll=&Deselect All +NoProjectsError=Select one or more projects to export. +StatusMessage=Creating Gradle build files... +FileStatusMessage=Generating build file for {0}... +WindowTitle=Export
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java new file mode 100644 index 000000000..6fbe14e42 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportStatus.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import java.io.File; + +public class ExportStatus { + + public static enum FileStatus { OK, VCS_FAILURE, IO_FAILURE; } + + private String mMainError = null; + private final Multimap<FileStatus, File> mFileStatus = ArrayListMultimap.create(); + + void addFileStatus(@NonNull FileStatus status, @NonNull File file) { + mFileStatus.put(status, file); + } + + boolean hasError() { + return mMainError != null || + !mFileStatus.get(FileStatus.VCS_FAILURE).isEmpty() || + !mFileStatus.get(FileStatus.IO_FAILURE).isEmpty(); + } + + public void setErrorMessage(String error) { + mMainError = error; + } + + @Nullable + public String getErrorMessage() { + return mMainError; + } + + @NonNull + public Multimap<FileStatus, File> getFileStatus() { + return mFileStatus; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java new file mode 100644 index 000000000..bbfadf855 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/FinalPage.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.android.ide.eclipse.adt.internal.wizards.exportgradle.ExportStatus.FileStatus; +import com.google.common.collect.Multimap; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.util.Collection; + +/** + * Final page to review the result of the export. + */ +public class FinalPage extends WizardPage { + + private final ProjectSetupBuilder mBuilder; + private ExportStatus mStatus; + + private Text mText; + + public FinalPage(ProjectSetupBuilder builder) { + super("FinalPage"); //$NON-NLS-1$ + mBuilder = builder; + setPageComplete(true); + setTitle(ExportMessages.PageTitle); + setDescription(ExportMessages.PageDescription); + } + + @Override + public void createControl(Composite parent) { + initializeDialogUnits(parent); + + mText = new Text(parent, SWT.MULTI | SWT.READ_ONLY); + mText.setLayoutData(new GridData(GridData.FILL_BOTH + | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL)); + + setControl(mText); + Dialog.applyDialogFont(parent); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + mStatus = mBuilder.getStatus(); + mBuilder.setCanFinish(!mStatus.hasError()); + mBuilder.setCanGenerate(false); + + StringBuilder sb = new StringBuilder(); + if (mStatus.hasError()) { + sb.append("There was an error!").append("\n\n"); + + String errorMsg = mStatus.getErrorMessage(); + if (errorMsg != null) { + sb.append(errorMsg); + } + + Multimap<FileStatus, File> fileStatusMap = mStatus.getFileStatus(); + Collection<File> files = fileStatusMap.values(); + if (files != null) { + sb.append("\n\n").append("Error on files:").append('\n'); + for (File file : files) { + sb.append("\n").append(file.getAbsolutePath()); + } + } + } else { + sb.append("Export successful.\n\n"); + + int count = mBuilder.getModuleCount(); + if (count > 1) { + sb.append(String.format("Exported %s modules", count)).append('\n'); + sb.append(String.format( + "Root folder: %s", mBuilder.getCommonRoot().toOSString())); + } else { + sb.append("Exported project: ").append(mBuilder.getCommonRoot().toOSString()); + } + + sb.append("\n\n").append("Choose 'Import Non-Android Studio project' in Android Studio").append('\n'); + sb.append("and select the following file:").append("\n\t"); + + File bGradle = new File( + mBuilder.getCommonRoot().toFile(), BuildFileCreator.BUILD_FILE); + sb.append(bGradle.getAbsolutePath()); + + sb.append("\n\n").append("Do NOT import the Eclipse project itself!"); + } + + mText.setText(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/exportgradle/GradleExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java new file mode 100644 index 000000000..8c74187ff --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.ui.IExportWizard; +import org.eclipse.ui.IWorkbench; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; + +public class GradleExportWizard extends Wizard implements IExportWizard { + + private ProjectSetupBuilder mBuilder = new ProjectSetupBuilder(); + + private ProjectSelectionPage mFirstPage; + private ConfirmationPage mSecondPage; + private FinalPage mFinalPage; + + /** + * Creates buildfile. + */ + @Override + public boolean performFinish() { + if (mBuilder.canGenerate()) { + generateBuildfiles(mSecondPage); + getContainer().showPage(mFinalPage); + return false; + } + + return true; + } + + @Override + public void addPages() { + addPage(new ImportInsteadPage()); + mFirstPage = new ProjectSelectionPage(mBuilder); + addPage(mFirstPage); + mSecondPage = new ConfirmationPage(mBuilder); + addPage(mSecondPage); + mFinalPage = new FinalPage(mBuilder); + addPage(mFinalPage); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + setWindowTitle(ExportMessages.WindowTitle); + setNeedsProgressMonitor(true); + } + + @Override + public boolean canFinish() { + return mBuilder.canFinish() || mBuilder.canGenerate(); + } + + /** + * Converts Eclipse Java projects to Gradle build files. Displays error dialogs. + */ + public boolean generateBuildfiles(final WizardPage page) { + IRunnableWithProgress runnable = new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor pm) throws InterruptedException { + Collection<GradleModule> modules = mBuilder.getModules(); + final int count = modules.size(); + + SubMonitor localmonitor = SubMonitor.convert(pm, ExportMessages.StatusMessage, + count); + BuildFileCreator.createBuildFiles( + mBuilder, + page.getShell(), + localmonitor.newChild(count)); + } + }; + + try { + getContainer().run(false, false, runnable); + } catch (InvocationTargetException e) { + AdtPlugin.log(e, null); + return false; + } catch (InterruptedException e) { + AdtPlugin.log(e, null); + return false; + } + if (page.getErrorMessage() != null) { + return false; + } + return true; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java new file mode 100644 index 000000000..684f03b9a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleModule.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.android.annotations.NonNull; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.IJavaProject; + +import java.util.List; + +/** + * A configured Gradle module for export. This includes gradle path, dependency, type, etc... + */ +public class GradleModule { + + @NonNull + private final IJavaProject mJavaProject; + + private String mPath; + private Type mType; + + private final List<GradleModule> mDependencies = Lists.newArrayList(); + + public static enum Type { ANDROID, JAVA }; + + GradleModule(@NonNull IJavaProject javaProject) { + mJavaProject = javaProject; + } + + @NonNull + public IJavaProject getJavaProject() { + return mJavaProject; + } + + @NonNull + public IProject getProject() { + return mJavaProject.getProject(); + } + + boolean isConfigured() { + return mType != null; + } + + public void setType(Type type) { + mType = type; + } + + public Type getType() { + return mType; + } + + public void addDependency(GradleModule module) { + mDependencies.add(module); + } + + public List<GradleModule> getDependencies() { + return mDependencies; + } + + public void setPath(String path) { + mPath = path; + } + + public String getPath() { + return mPath; + } + + @Override + public String toString() { + return "GradleModule [mJavaProject=" + mJavaProject + ", mPath=" + mPath + ", mType=" + + mType + ", mDependencies=" + mDependencies + "]"; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java new file mode 100644 index 000000000..cff9aca63 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ImportInsteadPage.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 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.exportgradle; + +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; + +class ImportInsteadPage extends WizardPage { + public ImportInsteadPage() { + super("importInstead"); + setTitle("Import Instead?"); + setDescription("Consider importing directly into Android Studio instead of exporting from Eclipse"); + } + + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + container.setLayout(new GridLayout(1, false)); + + CLabel label = new CLabel(container, SWT.NONE); + label.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, false, 1, 1)); + label.setText( + "Recent versions of Android Studio now support direct import of ADT projects.\n" + + "\n" + + "There are advantages to importing from Studio instead of exporting from Eclipse:\n" + + "- It can replace jars and library projects with Gradle dependencies instead\n" + + "- On import, it creates a new copy of the project and changes the project structure\n" + + " to the new Gradle directory layout which better supports multiple resource directories.\n" + + "- It can merge instrumentation test projects into the same project\n" + + "- Android Studio is released more frequently than the ADT plugin, so the import\n" + + " mechanism more closely tracks the requirements of Studio Gradle projects.\n" + + "\n" + + "If you want to preserve your Eclipse directory structure, or if for some reason import\n" + + "in Studio doesn't work (please let us know by filing a bug), continue to export from\n" + + "Eclipse instead."); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java new file mode 100644 index 000000000..81c7a7346 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSelectionPage.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.ibm.icu.text.MessageFormat; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaModelMarker; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.TableLayout; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.ui.model.WorkbenchContentProvider; +import org.eclipse.ui.model.WorkbenchLabelProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays a wizard page that lets the user choose the projects for which to create Gradle build + * files. + * <p> + * Based on {@link org.eclipse.ant.internal.ui.datatransfer.AntBuildfileExportPage} + */ +public class ProjectSelectionPage extends WizardPage { + + private final ProjectSetupBuilder mBuilder; + private CheckboxTableViewer mTableViewer; + private List<IJavaProject> mSelectedJavaProjects = Lists.newArrayList(); + + public ProjectSelectionPage(ProjectSetupBuilder builder) { + super("GradleExportPage"); //$NON-NLS-1$ + mBuilder = builder; + setPageComplete(false); + setTitle(ExportMessages.PageTitle); + setDescription(ExportMessages.PageDescription); + } + + @Override + public void createControl(Composite parent) { + initializeDialogUnits(parent); + + Composite workArea = new Composite(parent, SWT.NONE); + setControl(workArea); + + workArea.setLayout(new GridLayout()); + workArea.setLayoutData(new GridData(GridData.FILL_BOTH + | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL)); + + Label title = new Label(workArea, SWT.NONE); + title.setText(ExportMessages.SelectProjects); + + Composite listComposite = new Composite(workArea, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + layout.marginWidth = 0; + layout.makeColumnsEqualWidth = false; + listComposite.setLayout(layout); + + listComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL + | GridData.GRAB_VERTICAL | GridData.FILL_BOTH)); + + Table table = new Table(listComposite, + SWT.CHECK | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + mTableViewer = new CheckboxTableViewer(table); + table.setLayout(new TableLayout()); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + data.heightHint = 300; + table.setLayoutData(data); + mTableViewer.setContentProvider(new WorkbenchContentProvider() { + @Override + public Object[] getElements(Object element) { + if (element instanceof IJavaProject[]) { + return (IJavaProject[]) element; + } + return null; + } + }); + mTableViewer.setLabelProvider(new WorkbenchLabelProvider()); + mTableViewer.addCheckStateListener(new ICheckStateListener() { + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + if (event.getChecked()) { + mSelectedJavaProjects.add((IJavaProject) event.getElement()); + } else { + mSelectedJavaProjects.remove(event.getElement()); + } + updateEnablement(); + } + }); + + initializeProjects(); + createSelectionButtons(listComposite); + setControl(workArea); + updateEnablement(); + Dialog.applyDialogFont(parent); + } + + /** + * Creates select all/deselect all buttons. + */ + private void createSelectionButtons(Composite composite) { + Composite buttonsComposite = new Composite(composite, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.marginWidth = 0; + layout.marginHeight = 0; + buttonsComposite.setLayout(layout); + + buttonsComposite.setLayoutData(new GridData( + GridData.VERTICAL_ALIGN_BEGINNING)); + + Button selectAll = new Button(buttonsComposite, SWT.PUSH); + selectAll.setText(ExportMessages.SelectAll); + selectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + for (int i = 0; i < mTableViewer.getTable().getItemCount(); i++) { + mSelectedJavaProjects.add((IJavaProject) mTableViewer.getElementAt(i)); + } + mTableViewer.setAllChecked(true); + updateEnablement(); + } + }); + setButtonLayoutData(selectAll); + + Button deselectAll = new Button(buttonsComposite, SWT.PUSH); + deselectAll.setText(ExportMessages.DeselectAll); + deselectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSelectedJavaProjects.clear(); + mTableViewer.setAllChecked(false); + updateEnablement(); + } + }); + setButtonLayoutData(deselectAll); + } + + /** + * Populates the list with all the eligible projects in the workspace. + */ + private void initializeProjects() { + IWorkspaceRoot rootWorkspace = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(rootWorkspace); + IJavaProject[] javaProjects; + try { + javaProjects = javaModel.getJavaProjects(); + } catch (JavaModelException e) { + javaProjects = new IJavaProject[0]; + } + mTableViewer.setInput(javaProjects); + // Check any necessary projects + if (mSelectedJavaProjects != null) { + mTableViewer.setCheckedElements(mSelectedJavaProjects.toArray( + new IJavaProject[mSelectedJavaProjects.size()])); + } + } + + /** + * Enables/disables the finish button on the wizard and displays error messages as needed. + */ + private void updateEnablement() { + String error = null; + try { + if (mSelectedJavaProjects.size() == 0) { + error = ExportMessages.NoProjectsError; + return; + } + + List<String> cyclicProjects; + try { + cyclicProjects = getCyclicProjects(mSelectedJavaProjects); + if (cyclicProjects.size() > 0) { + error = MessageFormat.format(ExportMessages.CyclicProjectsError, + new Object[] { Joiner.on(", ").join(cyclicProjects) }); //$NON-NLS-1$ + return; + } + + error = mBuilder.setProject(mSelectedJavaProjects); + if (error != null) { + return; + } + + } catch (CoreException ignored) { + // TODO: do something? + } + } finally { + setErrorMessage(error); + setPageComplete(error == null); + getContainer().updateButtons(); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + mTableViewer.getTable().setFocus(); + mBuilder.setCanFinish(false); + mBuilder.setCanGenerate(false); + } + } + + /** + * Returns given projects that have cyclic dependencies. + * + * @param javaProjects list of IJavaProject objects + * @return set of project names + */ + private List<String> getCyclicProjects(List<IJavaProject> projects) throws CoreException { + + List<String> cyclicProjects = new ArrayList<String>(); + for (IJavaProject javaProject : projects) { + if (hasCyclicDependency(javaProject)) { + cyclicProjects.add(javaProject.getProject().getName()); + } + } + return cyclicProjects; + } + + /** + * Check if given project has a cyclic dependency. + * <p> + * See {@link org.eclipse.jdt.core.tests.model.ClasspathTests.numberOfCycleMarkers} + */ + private static boolean hasCyclicDependency(IJavaProject javaProject) + throws CoreException { + IMarker[] markers = javaProject.getProject().findMarkers( + IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, false, + IResource.DEPTH_ONE); + for (IMarker marker : markers) { + String cycleAttr = (String) marker + .getAttribute(IJavaModelMarker.CYCLE_DETECTED); + if (cycleAttr != null && cycleAttr.equals("true")) { //$NON-NLS-1$ + return true; + } + } + return false; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java new file mode 100644 index 000000000..1fd6b74f6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ProjectSetupBuilder.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2013 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.exportgradle; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import org.eclipse.core.resources.IProject; +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.Path; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Class to setup the project and its modules. + */ +public class ProjectSetupBuilder { + + private final static class InternalException extends Exception { + private static final long serialVersionUID = 1L; + + InternalException(String message) { + super(message); + } + } + + private boolean mCanFinish = false; + private boolean mCanGenerate = false; + private final List<GradleModule> mOriginalModules = Lists.newArrayList(); + private final Map<IJavaProject, GradleModule> mModules = Maps.newHashMap(); + private IPath mCommonRoot; + private ExportStatus mStatus; + + public ProjectSetupBuilder() { + + } + + public void setCanGenerate(boolean generate) { + mCanGenerate = generate; + } + + public void setCanFinish(boolean canFinish) { + mCanFinish = canFinish; + } + + public boolean canFinish() { + return mCanFinish; + } + + public boolean canGenerate() { + return mCanGenerate; + } + + public void setStatus(ExportStatus status) { + mStatus = status; + } + + public ExportStatus getStatus() { + return mStatus; + } + + @NonNull + public String setProject(@NonNull List<IJavaProject> selectedProjects) + throws CoreException { + mModules.clear(); + + // build a list of all projects that must be included. This is in case + // some dependencies have not been included in the selected projects. We also include + // parent projects so that the full multi-project setup is correct. + // Note that if two projects are selected that are not related, both will be added + // in the same multi-project anyway. + try { + for (IJavaProject javaProject : selectedProjects) { + GradleModule module; + + if (javaProject.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) { + module = processAndroidProject(javaProject); + } else { + module = processJavaProject(javaProject); + } + + mOriginalModules.add(module); + } + + Collection<GradleModule> modules = mModules.values(); + computeRootAndPaths(modules); + + return null; + } catch (InternalException e) { + return e.getMessage(); + } + } + + @NonNull + public Collection<GradleModule> getModules() { + return mModules.values(); + } + + public int getModuleCount() { + return mModules.size(); + } + + @Nullable + public IPath getCommonRoot() { + return mCommonRoot; + } + + @Nullable + public GradleModule getModule(IJavaProject javaProject) { + return mModules.get(javaProject); + } + + public boolean isOriginalProject(@NonNull IJavaProject javaProject) { + GradleModule module = mModules.get(javaProject); + return mOriginalModules.contains(module); + } + + @NonNull + public List<GradleModule> getOriginalModules() { + return mOriginalModules; + } + + @Nullable + public List<GradleModule> getShortestDependencyTo(GradleModule module) { + return findModule(module, mOriginalModules); + } + + @Nullable + public List<GradleModule> findModule(GradleModule toFind, GradleModule rootModule) { + if (toFind == rootModule) { + List<GradleModule> list = Lists.newArrayList(); + list.add(toFind); + return list; + } + + List<GradleModule> shortestChain = findModule(toFind, rootModule.getDependencies()); + + if (shortestChain != null) { + shortestChain.add(0, rootModule); + } + + return shortestChain; + } + + @Nullable + public List<GradleModule> findModule(GradleModule toFind, List<GradleModule> modules) { + List<GradleModule> currentChain = null; + + for (GradleModule child : modules) { + List<GradleModule> newChain = findModule(toFind, child); + if (currentChain == null) { + currentChain = newChain; + } else if (newChain != null) { + if (currentChain.size() > newChain.size()) { + currentChain = newChain; + } + } + } + + return currentChain; + } + + @NonNull + private GradleModule processAndroidProject(@NonNull IJavaProject javaProject) + throws InternalException, CoreException { + + // get/create the module + GradleModule module = createModuleOnDemand(javaProject); + if (module.isConfigured()) { + return module; + } + + module.setType(GradleModule.Type.ANDROID); + + ProjectState projectState = Sdk.getProjectState(javaProject.getProject()); + assert projectState != null; + + // add library project dependencies + List<LibraryState> libraryProjects = projectState.getLibraries(); + for (LibraryState libraryState : libraryProjects) { + ProjectState libProjectState = libraryState.getProjectState(); + if (libProjectState != null) { + IJavaProject javaLib = getJavaProject(libProjectState); + if (javaLib != null) { + GradleModule libModule = processAndroidProject(javaLib); + module.addDependency(libModule); + } else { + throw new InternalException(String.format( + "Project %1$s is missing. Needed by %2$s.\n" + + "Make sure all dependencies are opened.", + libraryState.getRelativePath(), + javaProject.getProject().getName())); + } + } else { + throw new InternalException(String.format( + "Project %1$s is missing. Needed by %2$s.\n" + + "Make sure all dependencies are opened.", + libraryState.getRelativePath(), + javaProject.getProject().getName())); + } + } + + // add java project dependencies + List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject); + for (IJavaProject javaDep : javaDepProjects) { + GradleModule libModule = processJavaProject(javaDep); + module.addDependency(libModule); + } + + return module; + } + + @NonNull + private GradleModule processJavaProject(@NonNull IJavaProject javaProject) + throws InternalException, CoreException { + // get/create the module + GradleModule module = createModuleOnDemand(javaProject); + + if (module.isConfigured()) { + return module; + } + + module.setType(GradleModule.Type.JAVA); + + // add java project dependencies + List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject); + for (IJavaProject javaDep : javaDepProjects) { + // Java project should not reference Android project! + if (javaDep.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) { + throw new InternalException(String.format( + "Java project %1$s depends on Android project %2$s!\n" + + "This is not a valid dependency", + javaProject.getProject().getName(), javaDep.getProject().getName())); + } + GradleModule libModule = processJavaProject(javaDep); + module.addDependency(libModule); + } + + return module; + } + + private void computeRootAndPaths(Collection<GradleModule> modules) throws InternalException { + // compute the common root. + mCommonRoot = determineCommonRoot(modules); + + // compute all the relative paths. + for (GradleModule module : modules) { + String path = getGradlePath(module.getJavaProject().getProject().getLocation(), + mCommonRoot); + + module.setPath(path); + } + } + + /** + * Finds the common parent directory shared by this project and all its dependencies. + * If there's only one project, returns the single project's folder. + * @throws InternalException + */ + @NonNull + private static IPath determineCommonRoot(Collection<GradleModule> modules) + throws InternalException { + IPath commonRoot = null; + for (GradleModule module : modules) { + if (commonRoot == null) { + commonRoot = module.getJavaProject().getProject().getLocation(); + } else { + commonRoot = findCommonRoot(commonRoot, + module.getJavaProject().getProject().getLocation()); + } + } + + return commonRoot; + } + + /** + * Converts the given path to be relative to the given root path, and converts it to + * Gradle project notation, such as is used in the settings.gradle file. + */ + @NonNull + private static String getGradlePath(IPath path, IPath root) { + IPath relativePath = path.makeRelativeTo(root); + String relativeString = relativePath.toOSString(); + return ":" + relativeString.replaceAll(Pattern.quote(File.separator), ":"); //$NON-NLS-1$ + } + + /** + * Given two IPaths, finds the parent directory of both of them. + * @throws InternalException + */ + @NonNull + private static IPath findCommonRoot(@NonNull IPath path1, @NonNull IPath path2) + throws InternalException { + if (path1.getDevice() != null && !path1.getDevice().equals(path2.getDevice())) { + throw new InternalException( + "Different modules have been detected on different drives.\n" + + "This prevents finding a common root to all modules."); + } + + IPath result = path1.uptoSegment(0); + + final int count = Math.min(path1.segmentCount(), path2.segmentCount()); + for (int i = 0; i < count; i++) { + if (path1.segment(i).equals(path2.segment(i))) { + result = result.append(Path.SEPARATOR + path2.segment(i)); + } + } + return result; + } + + @Nullable + private IJavaProject getJavaProject(ProjectState projectState) { + try { + return BaseProjectHelper.getJavaProject(projectState.getProject()); + } catch (CoreException e) { + return null; + } + } + + @NonNull + private GradleModule createModuleOnDemand(@NonNull IJavaProject javaProject) { + GradleModule module = mModules.get(javaProject); + if (module == null) { + module = new GradleModule(javaProject); + mModules.put(javaProject, module); + } + + return module; + } + + @NonNull + private static List<IJavaProject> getReferencedProjects(IJavaProject javaProject) + throws JavaModelException, InternalException { + + List<IJavaProject> projects = Lists.newArrayList(); + + IClasspathEntry entries[] = javaProject.getRawClasspath(); + for (IClasspathEntry classpathEntry : entries) { + if (classpathEntry.getContentKind() == IPackageFragmentRoot.K_SOURCE + && classpathEntry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { + // found required project on build path + String subProjectRoot = classpathEntry.getPath().toString(); + IJavaProject subProject = getJavaProject(subProjectRoot); + // is project available in workspace? + if (subProject != null) { + projects.add(subProject); + } else { + throw new InternalException(String.format( + "Project '%s' is missing project dependency '%s' in Eclipse workspace.\n" + + "Make sure all dependencies are opened.", + javaProject.getProject().getName(), + classpathEntry.getPath().toString())); + } + } + } + + return projects; + } + + /** + * Get Java project for given root. + */ + @Nullable + private static IJavaProject getJavaProject(String root) { + IPath path = new Path(root); + if (path.segmentCount() == 1) { + return getJavaProjectByName(root); + } + IResource resource = ResourcesPlugin.getWorkspace().getRoot() + .findMember(path); + if (resource != null && resource.getType() == IResource.PROJECT) { + if (resource.exists()) { + return (IJavaProject) JavaCore.create(resource); + } + } + return null; + } + + /** + * Get Java project from resource. + */ + private static IJavaProject getJavaProjectByName(String name) { + try { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (project.exists()) { + return JavaCore.create(project); + } + } catch (IllegalArgumentException iae) { + } + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java new file mode 100644 index 000000000..c8325345a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ApplicationInfoPage.java @@ -0,0 +1,809 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.resources.IProject; +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.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.JavaConventions; +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.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.io.FileFilter; +import java.net.URI; + +/** Page where you choose the application name, activity name, and optional test project info */ +public class ApplicationInfoPage extends WizardPage implements SelectionListener, ModifyListener, + ITargetChangeListener { + private static final String JDK_15 = "1.5"; //$NON-NLS-1$ + private final static String DUMMY_PACKAGE = "your.package.namespace"; + + /** Suffix added by default to activity names */ + static final String ACTIVITY_NAME_SUFFIX = "Activity"; //$NON-NLS-1$ + + private final NewProjectWizardState mValues; + + private Text mApplicationText; + private Text mPackageText; + private Text mActivityText; + private Button mCreateActivityCheckbox; + private Combo mSdkCombo; + + private boolean mIgnore; + private Button mCreateTestCheckbox; + private Text mTestProjectNameText; + private Text mTestApplicationText; + private Text mTestPackageText; + private Label mTestProjectNameLabel; + private Label mTestApplicationLabel; + private Label mTestPackageLabel; + + /** + * Create the wizard. + */ + ApplicationInfoPage(NewProjectWizardState values) { + super("appInfo"); //$NON-NLS-1$ + mValues = values; + + setTitle("Application Info"); + setDescription("Configure the new Android Project"); + AdtPlugin.getDefault().addTargetListener(this); + } + + /** + * Create contents of the wizard. + */ + @Override + @SuppressWarnings("unused") // Eclipse marks SWT constructors with side effects as unused + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(2, false)); + + Label applicationLabel = new Label(container, SWT.NONE); + applicationLabel.setText("Application Name:"); + + mApplicationText = new Text(container, SWT.BORDER); + mApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mApplicationText.addModifyListener(this); + + Label packageLabel = new Label(container, SWT.NONE); + packageLabel.setText("Package Name:"); + + mPackageText = new Text(container, SWT.BORDER); + mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mPackageText.addModifyListener(this); + + if (mValues.mode != Mode.TEST) { + mCreateActivityCheckbox = new Button(container, SWT.CHECK); + mCreateActivityCheckbox.setText("Create Activity:"); + mCreateActivityCheckbox.addSelectionListener(this); + + mActivityText = new Text(container, SWT.BORDER); + mActivityText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mActivityText.addModifyListener(this); + } + + Label minSdkLabel = new Label(container, SWT.NONE); + minSdkLabel.setText("Minimum SDK:"); + + mSdkCombo = new Combo(container, SWT.NONE); + GridData gdSdkCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1); + gdSdkCombo.widthHint = 200; + mSdkCombo.setLayoutData(gdSdkCombo); + mSdkCombo.addSelectionListener(this); + mSdkCombo.addModifyListener(this); + + onSdkLoaded(); + + setControl(container); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + + mCreateTestCheckbox = new Button(container, SWT.CHECK); + mCreateTestCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mCreateTestCheckbox.setText("Create a Test Project"); + mCreateTestCheckbox.addSelectionListener(this); + + mTestProjectNameLabel = new Label(container, SWT.NONE); + mTestProjectNameLabel.setText("Test Project Name:"); + + mTestProjectNameText = new Text(container, SWT.BORDER); + mTestProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestProjectNameText.addModifyListener(this); + + mTestApplicationLabel = new Label(container, SWT.NONE); + mTestApplicationLabel.setText("Test Application:"); + + mTestApplicationText = new Text(container, SWT.BORDER); + mTestApplicationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestApplicationText.addModifyListener(this); + + mTestPackageLabel = new Label(container, SWT.NONE); + mTestPackageLabel.setText("Test Package:"); + + mTestPackageText = new Text(container, SWT.BORDER); + mTestPackageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mTestPackageText.addModifyListener(this); + } + + /** Controls whether the options for creating a paired test project should be shown */ + private void showTestOptions(boolean visible) { + if (mValues.mode == Mode.SAMPLE) { + visible = false; + } + + mCreateTestCheckbox.setVisible(visible); + mTestProjectNameLabel.setVisible(visible); + mTestProjectNameText.setVisible(visible); + mTestApplicationLabel.setVisible(visible); + mTestApplicationText.setVisible(visible); + mTestPackageLabel.setVisible(visible); + mTestPackageText.setVisible(visible); + } + + /** Controls whether the options for creating a paired test project should be enabled */ + private void enableTestOptions(boolean enabled) { + mTestProjectNameLabel.setEnabled(enabled); + mTestProjectNameText.setEnabled(enabled); + mTestApplicationLabel.setEnabled(enabled); + mTestApplicationText.setEnabled(enabled); + mTestPackageLabel.setEnabled(enabled); + mTestPackageText.setEnabled(enabled); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + try { + mIgnore = true; + if (mValues.applicationName != null) { + mApplicationText.setText(mValues.applicationName); + } + if (mValues.packageName != null) { + mPackageText.setText(mValues.packageName); + } else { + mPackageText.setText(DUMMY_PACKAGE); + } + + if (mValues.mode != Mode.TEST) { + mCreateActivityCheckbox.setSelection(mValues.createActivity); + mActivityText.setEnabled(mValues.createActivity); + if (mValues.activityName != null) { + mActivityText.setText(mValues.activityName); + } + } + if (mValues.minSdk != null && mValues.minSdk.length() > 0) { + mSdkCombo.setText(mValues.minSdk); + } + + showTestOptions(mValues.mode == Mode.ANY); + enableTestOptions(mCreateTestCheckbox.getSelection()); + + if (mValues.testProjectName != null) { + mTestProjectNameText.setText(mValues.testProjectName); + } + if (mValues.testApplicationName != null) { + mTestApplicationText.setText(mValues.testApplicationName); + } + if (mValues.testProjectName != null) { + mTestPackageText.setText(mValues.testProjectName); + } + } finally { + mIgnore = false; + } + } + + // Start focus with the package name, since the other fields are typically assigned + // reasonable defaults + mPackageText.setFocus(); + mPackageText.selectAll(); + + validatePage(); + } + + protected void setSdkTargets(IAndroidTarget[] targets, IAndroidTarget target) { + if (targets == null) { + targets = new IAndroidTarget[0]; + } + int selectionIndex = -1; + String[] items = new String[targets.length]; + for (int i = 0, n = targets.length; i < n; i++) { + items[i] = targetLabel(targets[i]); + if (targets[i] == target) { + selectionIndex = i; + } + } + try { + mIgnore = true; + mSdkCombo.setItems(items); + mSdkCombo.setData(targets); + if (selectionIndex != -1) { + mSdkCombo.select(selectionIndex); + } + } finally { + mIgnore = false; + } + } + + private String targetLabel(IAndroidTarget target) { + // In the minimum SDK chooser, show the targets with api number and description, + // such as "11 (Android 3.0)" + return String.format("%1$s (%2$s)", target.getVersion().getApiString(), + target.getFullName()); + } + + @Override + public void dispose() { + AdtPlugin.getDefault().removeTargetListener(this); + super.dispose(); + } + + @Override + public boolean isPageComplete() { + // This page is only needed when creating new projects + if (mValues.useExisting || mValues.mode != Mode.ANY) { + return true; + } + + // Ensure that we reach this page + if (mValues.packageName == null) { + return false; + } + + return super.isPageComplete(); + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mSdkCombo) { + mValues.minSdk = mSdkCombo.getText().trim(); + IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData(); + // An editable combo will treat item selection the same way as a user edit, + // so we need to see if the string looks like a labeled version + int index = mSdkCombo.getSelectionIndex(); + if (index != -1) { + if (index >= 0 && index < targets.length) { + IAndroidTarget target = targets[index]; + if (targetLabel(target).equals(mValues.minSdk)) { + mValues.minSdk = target.getVersion().getApiString(); + } + } + } + + // Ensure that we never pick up the (Android x.y) suffix shown in combobox + // for readability + int separator = mValues.minSdk.indexOf(' '); + if (separator != -1) { + mValues.minSdk = mValues.minSdk.substring(0, separator); + } + mValues.minSdkModifiedByUser = true; + mValues.updateSdkTargetToMatchMinSdkVersion(); + } else if (source == mApplicationText) { + mValues.applicationName = mApplicationText.getText().trim(); + mValues.applicationNameModifiedByUser = true; + + if (!mValues.testApplicationNameModified) { + mValues.testApplicationName = suggestTestApplicationName(mValues.applicationName); + try { + mIgnore = true; + mTestApplicationText.setText(mValues.testApplicationName); + } finally { + mIgnore = false; + } + } + + } else if (source == mPackageText) { + mValues.packageName = mPackageText.getText().trim(); + mValues.packageNameModifiedByUser = true; + + if (!mValues.testPackageModified) { + mValues.testPackageName = suggestTestPackage(mValues.packageName); + try { + mIgnore = true; + mTestPackageText.setText(mValues.testPackageName); + } finally { + mIgnore = false; + } + } + } else if (source == mActivityText) { + mValues.activityName = mActivityText.getText().trim(); + mValues.activityNameModifiedByUser = true; + } else if (source == mTestApplicationText) { + mValues.testApplicationName = mTestApplicationText.getText().trim(); + mValues.testApplicationNameModified = true; + } else if (source == mTestPackageText) { + mValues.testPackageName = mTestPackageText.getText().trim(); + mValues.testPackageModified = true; + } else if (source == mTestProjectNameText) { + mValues.testProjectName = mTestProjectNameText.getText().trim(); + mValues.testProjectModified = true; + } + + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mCreateActivityCheckbox) { + mValues.createActivity = mCreateActivityCheckbox.getSelection(); + mActivityText.setEnabled(mValues.createActivity); + } else if (source == mSdkCombo) { + int index = mSdkCombo.getSelectionIndex(); + IAndroidTarget[] targets = (IAndroidTarget[]) mSdkCombo.getData(); + if (index != -1) { + if (index >= 0 && index < targets.length) { + IAndroidTarget target = targets[index]; + // Even though we are showing the logical version name, we place the + // actual api number as the minimum SDK + mValues.minSdk = target.getVersion().getApiString(); + } + } else { + String text = mSdkCombo.getText(); + boolean found = false; + for (IAndroidTarget target : targets) { + if (targetLabel(target).equals(text)) { + mValues.minSdk = target.getVersion().getApiString(); + found = true; + break; + } + } + if (!found) { + mValues.minSdk = text; + } + } + } else if (source == mCreateTestCheckbox) { + mValues.createPairProject = mCreateTestCheckbox.getSelection(); + enableTestOptions(mValues.createPairProject); + if (mValues.createPairProject) { + if (mValues.testProjectName == null || mValues.testProjectName.length() == 0) { + mValues.testProjectName = suggestTestProjectName(mValues.projectName); + } + if (mValues.testApplicationName == null || + mValues.testApplicationName.length() == 0) { + mValues.testApplicationName = + suggestTestApplicationName(mValues.applicationName); + } + if (mValues.testPackageName == null || mValues.testPackageName.length() == 0) { + mValues.testPackageName = suggestTestPackage(mValues.packageName); + } + + try { + mIgnore = true; + mTestProjectNameText.setText(mValues.testProjectName); + mTestApplicationText.setText(mValues.testApplicationName); + mTestPackageText.setText(mValues.testPackageName); + } finally { + mIgnore = false; + } + } + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + private void validatePage() { + IStatus status = validatePackage(mValues.packageName); + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validActivity = validateActivity(); + if (validActivity != null) { + status = validActivity; + } + } + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validMinSdk = validateMinSdk(); + if (validMinSdk != null) { + status = validMinSdk; + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validSourceFolder = validateSourceFolder(); + if (validSourceFolder != null) { + status = validSourceFolder; + } + } + + // If creating a test project to go along with the main project, also validate + // the additional test project parameters + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.createPairProject) { + IStatus validTestProject = ProjectNamePage.validateProjectName( + mValues.testProjectName); + if (validTestProject != null) { + status = validTestProject; + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validTestLocation = validateTestProjectLocation(); + if (validTestLocation != null) { + status = validTestLocation; + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validTestPackage = validatePackage(mValues.testPackageName); + if (validTestPackage != null) { + status = new Status(validTestPackage.getSeverity(), + AdtPlugin.PLUGIN_ID, + validTestPackage.getMessage() + " (in test package)"); + } + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + if (mValues.projectName.equals(mValues.testProjectName)) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "The main project name and the test project name must be different."); + } + } + } + } + + // -- update UI & enable finish if there's no error + 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 validateTestProjectLocation() { + assert mValues.createPairProject; + + // Validate location + Path path = new Path(mValues.projectLocation.getPath()); + if (!mValues.useExisting) { + if (!mValues.useDefaultLocation) { + // If not using the default value validate the location. + URI uri = URIUtil.toURI(path.toOSString()); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject handle = workspace.getRoot().getProject(mValues.testProjectName); + IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri); + if (!locationStatus.isOK()) { + return locationStatus; + } + // The location is valid as far as Eclipse is concerned (i.e. mostly not + // an existing workspace project.) Check it either doesn't exist or is + // a directory that is empty. + File f = path.toFile(); + if (f.exists() && !f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } else if (f.isDirectory()) { + // However if the directory exists, we should put a + // warning if it is not empty. We don't put an error + // (we'll ask the user again for confirmation before + // using the directory.) + String[] l = f.list(); + if (l != null && l.length != 0) { + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "The selected output directory is not empty."); + } + } + } else { + IPath destPath = path.removeLastSegments(1).append(mValues.testProjectName); + File dest = destPath.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.", + mValues.testProjectName)); + } + } + } + + return null; + } + + private IStatus validateSourceFolder() { + // This check does nothing when creating a new project. + // This check is also useless when no activity is present or created. + mValues.sourceFolder = SdkConstants.FD_SOURCES; + if (!mValues.useExisting || !mValues.createActivity) { + return null; + } + + String osTarget = mValues.activityName; + if (osTarget.indexOf('.') == -1) { + osTarget = mValues.packageName + File.separator + osTarget; + } else if (osTarget.indexOf('.') == 0) { + osTarget = mValues.packageName + osTarget; + } + osTarget = osTarget.replace('.', File.separatorChar) + SdkConstants.DOT_JAVA; + + File projectDir = mValues.projectLocation; + File[] allDirs = projectDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (allDirs != null) { + boolean found = false; + for (File f : allDirs) { + Path path = new Path(f.getAbsolutePath()); + File java_activity = path.append(osTarget).toFile(); + if (java_activity.isFile()) { + mValues.sourceFolder = f.getName(); + found = true; + break; + } + } + + if (!found) { + String projectPath = projectDir.getPath(); + if (allDirs.length > 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("%1$s can not be found under %2$s.", osTarget, + projectPath)); + } else { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No source folders can be found in %1$s.", + projectPath)); + } + } + } + + return null; + } + + private IStatus validateMinSdk() { + // Validate min SDK field + // If the min sdk version is empty, it is always accepted. + if (mValues.minSdk == null || mValues.minSdk.length() == 0) { + return null; + } + + IAndroidTarget target = mValues.target; + if (target == null) { + return null; + } + + // If the current target is a preview, explicitly indicate minSdkVersion + // must be set to this target name. + if (target.getVersion().isPreview() && !target.getVersion().equals(mValues.minSdk)) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "The SDK target is a preview. Min SDK Version must be set to '%s'.", + target.getVersion().getCodename())); + } + + if (!target.getVersion().equals(mValues.minSdk)) { + return new Status(target.getVersion().isPreview() ? IStatus.ERROR : IStatus.WARNING, + AdtPlugin.PLUGIN_ID, + "The API level for the selected SDK target does not match the Min SDK Version." + ); + } + + return null; + } + + public static IStatus validatePackage(String packageFieldContents) { + // Validate package + if (packageFieldContents == null || packageFieldContents.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must be specified."); + } else if (packageFieldContents.equals(DUMMY_PACKAGE)) { + // The dummy package name is just a placeholder package (which isn't even valid + // because it contains the reserved Java keyword "package") but we want to + // make the error message say that a proper package should be entered rather than + // what's wrong with this specific package. (And the reason we provide a dummy + // package rather than a blank line is to make it more clear to beginners what + // we're looking for. + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must be specified."); + } + // Check it's a valid package string + IStatus status = JavaConventions.validatePackageName(packageFieldContents, JDK_15, + JDK_15); + if (!status.isOK()) { + return status; + } + + // The Android Activity Manager does not accept packages names with only one + // identifier. Check the package name has at least one dot in them (the previous rule + // validated that if such a dot exist, it's not the first nor last characters of the + // string.) + if (packageFieldContents.indexOf('.') == -1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package name must have at least two identifiers."); + } + + return null; + } + + public static IStatus validateClass(String className) { + if (className == null || className.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Class name must be specified."); + } + if (className.indexOf('.') != -1) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Enter just a class name, not a full package name"); + } + return JavaConventions.validateJavaTypeName(className, JDK_15, JDK_15); + } + + private IStatus validateActivity() { + // Validate activity (if creating an activity) + if (!mValues.createActivity) { + return null; + } + + return validateActivity(mValues.activityName); + } + + /** + * Validates the given activity name + * + * @param activityFieldContents the activity name to validate + * @return a status for whether the activity name is valid + */ + public static IStatus validateActivity(String activityFieldContents) { + // Validate activity field + if (activityFieldContents == null || activityFieldContents.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Activity name must be specified."); + } else if (ACTIVITY_NAME_SUFFIX.equals(activityFieldContents)) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Enter a valid activity name"); + } else if (activityFieldContents.contains("..")) { //$NON-NLS-1$ + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Package segments in activity name cannot be empty (..)"); + } + // The activity field can actually contain part of a sub-package name + // or it can start with a dot "." to indicates it comes from the parent package + // name. + String packageName = ""; //$NON-NLS-1$ + int pos = activityFieldContents.lastIndexOf('.'); + if (pos >= 0) { + packageName = activityFieldContents.substring(0, pos); + if (packageName.startsWith(".")) { //$NON-NLS-1$ + packageName = packageName.substring(1); + } + + activityFieldContents = activityFieldContents.substring(pos + 1); + } + + // the activity field can contain a simple java identifier, or a + // package name or one that starts with a dot. So if it starts with a dot, + // ignore this dot -- the rest must look like a package name. + if (activityFieldContents.length() > 0 && activityFieldContents.charAt(0) == '.') { + activityFieldContents = activityFieldContents.substring(1); + } + + // Check it's a valid activity string + IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents, JDK_15, + JDK_15); + if (!status.isOK()) { + return status; + } + + // Check it's a valid package string + if (packageName.length() > 0) { + status = JavaConventions.validatePackageName(packageName, JDK_15, JDK_15); + if (!status.isOK()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + status.getMessage() + " (in the activity name)"); + } + } + + return null; + } + + // ---- Implement ITargetChangeListener ---- + + @Override + public void onSdkLoaded() { + if (mSdkCombo == null) { + return; + } + + // Update the sdk target selector with the new targets + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + setSdkTargets(targets, mValues.target); + } + + @Override + public void onProjectTargetChange(IProject changedProject) { + // Ignore + } + + @Override + public void onTargetLoaded(IAndroidTarget target) { + // Ignore + } + + public static String suggestTestApplicationName(String applicationName) { + if (applicationName == null) { + applicationName = ""; //$NON-NLS-1$ + } + if (applicationName.indexOf(' ') != -1) { + return applicationName + " Test"; //$NON-NLS-1$ + } else { + return applicationName + "Test"; //$NON-NLS-1$ + } + } + + public static String suggestTestProjectName(String projectName) { + if (projectName == null) { + projectName = ""; //$NON-NLS-1$ + } + if (projectName.length() > 0 && Character.isUpperCase(projectName.charAt(0))) { + return projectName + "Test"; //$NON-NLS-1$ + } else { + return projectName + "-test"; //$NON-NLS-1$ + } + } + + + public static String suggestTestPackage(String packagePath) { + if (packagePath == null) { + packagePath = ""; //$NON-NLS-1$ + } + return packagePath + ".test"; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java new file mode 100755 index 000000000..0f4e87e9f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/FileStoreAdapter.java @@ -0,0 +1,160 @@ +/* + * 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.newproject; + +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.IFileSystem; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +/** + * IFileStore implementation that delegates to the give {@link IFileStore}. + * This makes it easier to just override a single method from a store. + */ +class FileStoreAdapter implements IFileStore { + + private final IFileStore mStore; + + public FileStoreAdapter(IFileStore store) { + mStore = store; + } + + @SuppressWarnings("rawtypes") + @Override + public Object getAdapter(Class adapter) { + return mStore.getAdapter(adapter); + } + + @Override + public IFileInfo[] childInfos(int options, IProgressMonitor monitor) throws CoreException { + return mStore.childInfos(options, monitor); + } + + @Override + public String[] childNames(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.childNames(options, monitor); + } + + @Override + public IFileStore[] childStores(int options, IProgressMonitor monitor) throws CoreException { + return mStore.childStores(options, monitor); + } + + @Override + public void copy(IFileStore destination, int options, IProgressMonitor monitor) + throws CoreException { + mStore.copy(destination, options, monitor); + } + + @Override + public void delete(int options, IProgressMonitor monitor) throws CoreException { + mStore.delete(options, monitor); + } + + @Override + public IFileInfo fetchInfo() { + return mStore.fetchInfo(); + } + + @Override + public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException { + return mStore.fetchInfo(options, monitor); + } + + @Deprecated + @Override + public IFileStore getChild(IPath path) { + return mStore.getChild(path); + } + + @Override + public IFileStore getFileStore(IPath path) { + return mStore.getFileStore(path); + } + + @Override + public IFileStore getChild(String name) { + return mStore.getChild(name); + } + + @Override + public IFileSystem getFileSystem() { + return mStore.getFileSystem(); + } + + @Override + public String getName() { + return mStore.getName(); + } + + @Override + public IFileStore getParent() { + return mStore.getParent(); + } + + @Override + public boolean isParentOf(IFileStore other) { + return mStore.isParentOf(other); + } + + @Override + public IFileStore mkdir(int options, IProgressMonitor monitor) throws CoreException { + return mStore.mkdir(options, monitor); + } + + @Override + public void move(IFileStore destination, int options, IProgressMonitor monitor) + throws CoreException { + mStore.move(destination, options, monitor); + } + + @Override + public InputStream openInputStream(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.openInputStream(options, monitor); + } + + @Override + public OutputStream openOutputStream(int options, IProgressMonitor monitor) + throws CoreException { + return mStore.openOutputStream(options, monitor); + } + + @Override + public void putInfo(IFileInfo info, int options, IProgressMonitor monitor) + throws CoreException { + mStore.putInfo(info, options, monitor); + } + + @Override + public File toLocalFile(int options, IProgressMonitor monitor) throws CoreException { + return mStore.toLocalFile(options, monitor); + } + + @Override + public URI toURI() { + return mStore.toURI(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java new file mode 100644 index 000000000..1e02fedae --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java @@ -0,0 +1,512 @@ +/* + * 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.newproject; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.tools.lint.detector.api.LintUtils; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Rectangle; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** WizardPage for importing Android projects */ +class ImportPage extends WizardPage implements SelectionListener, IStructuredContentProvider, + ICheckStateListener, KeyListener, TraverseListener, ControlListener { + private static final int DIR_COLUMN = 0; + private static final int NAME_COLUMN = 1; + + private final NewProjectWizardState mValues; + private List<ImportedProject> mProjectPaths; + private final IProject[] mExistingProjects; + + private Text mDir; + private Button mBrowseButton; + private Button mCopyCheckBox; + private Button mRefreshButton; + private Button mDeselectAllButton; + private Button mSelectAllButton; + private Table mTable; + private CheckboxTableViewer mCheckboxTableViewer; + private WorkingSetGroup mWorkingSetGroup; + + ImportPage(NewProjectWizardState values) { + super("importPage"); //$NON-NLS-1$ + mValues = values; + setTitle("Import Projects"); + setDescription("Select a directory to search for existing Android projects"); + mWorkingSetGroup = new WorkingSetGroup(); + setWorkingSets(new IWorkingSet[0]); + + // Record all projects such that we can ensure that the project names are unique + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + mExistingProjects = workspaceRoot.getProjects(); + } + + public void init(IStructuredSelection selection, IWorkbenchPart activePart) { + setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart)); + } + + @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 directoryLabel = new Label(container, SWT.NONE); + directoryLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + directoryLabel.setText("Root Directory:"); + + mDir = new Text(container, SWT.BORDER); + mDir.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mDir.addKeyListener(this); + mDir.addTraverseListener(this); + + mBrowseButton = new Button(container, SWT.NONE); + mBrowseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mBrowseButton.setText("Browse..."); + mBrowseButton.addSelectionListener(this); + + Label projectsLabel = new Label(container, SWT.NONE); + projectsLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + projectsLabel.setText("Projects:"); + + mTable = new Table(container, SWT.CHECK); + mTable.setHeaderVisible(true); + mCheckboxTableViewer = new CheckboxTableViewer(mTable); + + TableViewerColumn dirViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn dirColumn = dirViewerColumn.getColumn(); + dirColumn.setWidth(200); + dirColumn.setText("Project to Import"); + TableViewerColumn nameViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn nameColumn = nameViewerColumn.getColumn(); + nameColumn.setWidth(200); + nameColumn.setText("New Project Name"); + nameViewerColumn.setEditingSupport(new ProjectNameEditingSupport(mCheckboxTableViewer)); + + mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 4)); + mTable.setLinesVisible(true); + mTable.setHeaderVisible(true); + mTable.addSelectionListener(this); + mTable.addControlListener(this); + mCheckboxTableViewer.setContentProvider(this); + mCheckboxTableViewer.setInput(this); + mCheckboxTableViewer.addCheckStateListener(this); + mCheckboxTableViewer.setLabelProvider(new ProjectCellLabelProvider()); + + mSelectAllButton = new Button(container, SWT.NONE); + mSelectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mSelectAllButton.setText("Select All"); + mSelectAllButton.addSelectionListener(this); + + mDeselectAllButton = new Button(container, SWT.NONE); + mDeselectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mDeselectAllButton.setText("Deselect All"); + mDeselectAllButton.addSelectionListener(this); + + mRefreshButton = new Button(container, SWT.NONE); + mRefreshButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mRefreshButton.setText("Refresh"); + mRefreshButton.addSelectionListener(this); + new Label(container, SWT.NONE); + + mCopyCheckBox = new Button(container, SWT.CHECK); + mCopyCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mCopyCheckBox.setText("Copy projects into workspace"); + mCopyCheckBox.addSelectionListener(this); + + Composite group = mWorkingSetGroup.createControl(container); + group.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); + + updateColumnWidths(); + } + + private void updateColumnWidths() { + Rectangle r = mTable.getClientArea(); + int availableWidth = r.width; + // Add all available size to the first column + for (int i = 1; i < mTable.getColumnCount(); i++) { + TableColumn column = mTable.getColumn(i); + availableWidth -= column.getWidth(); + } + if (availableWidth > 100) { + mTable.getColumn(0).setWidth(availableWidth); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + validatePage(); + } + + private void refresh() { + File root = new File(mDir.getText().trim()); + mProjectPaths = searchForProjects(root); + mCheckboxTableViewer.refresh(); + mCheckboxTableViewer.setAllChecked(true); + + updateValidity(); + validatePage(); + } + + private void updateValidity(){ + List<ImportedProject> selected = new ArrayList<ImportedProject>(); + List<ImportedProject> disabled = new ArrayList<ImportedProject>(); + for (ImportedProject project : mProjectPaths) { + String projectName = project.getProjectName(); + boolean invalid = false; + for (IProject existingProject : mExistingProjects) { + if (projectName.equals(existingProject.getName())) { + invalid = true; + break; + } + } + if (invalid) { + disabled.add(project); + } else { + selected.add(project); + } + } + + mValues.importProjects = selected; + + mCheckboxTableViewer.setGrayedElements(disabled.toArray()); + mCheckboxTableViewer.setCheckedElements(selected.toArray()); + mCheckboxTableViewer.refresh(); + mCheckboxTableViewer.getTable().setFocus(); + } + + private List<ImportedProject> searchForProjects(File dir) { + List<ImportedProject> projects = new ArrayList<ImportedProject>(); + addProjects(dir, projects, dir.getPath().length() + 1); + return projects; + } + + /** Finds all project directories under the given directory */ + private void addProjects(File dir, List<ImportedProject> projects, int prefixLength) { + if (dir.isDirectory()) { + if (LintUtils.isManifestFolder(dir)) { + String relative = dir.getPath(); + if (relative.length() > prefixLength) { + relative = relative.substring(prefixLength); + } + projects.add(new ImportedProject(dir, relative)); + } + + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + addProjects(child, projects, prefixLength); + } + } + } + } + + private void validatePage() { + IStatus status = null; + + // Validate project name -- unless we're creating a sample, in which case + // the user will get a chance to pick the name on the Sample page + if (mProjectPaths == null || mProjectPaths.isEmpty()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Select a directory to search for existing Android projects"); + } else if (mValues.importProjects == null || mValues.importProjects.isEmpty()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Select at least one project"); + } else { + for (ImportedProject project : mValues.importProjects) { + if (mCheckboxTableViewer.getGrayed(project)) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("Cannot import %1$s because the project name is in use", + project.getProjectName())); + break; + } else { + status = ProjectNamePage.validateProjectName(project.getProjectName()); + if (status != null && !status.isOK()) { + // Need to insert project name to make it clear which project name + // is in violation + if (mValues.importProjects.size() > 1) { + String message = String.format("%1$s: %2$s", + project.getProjectName(), status.getMessage()); + status = new Status(status.getSeverity(), AdtPlugin.PLUGIN_ID, + message); + } + break; + } else { + status = null; // Don't leave non null status with isOK() == true + } + } + } + } + + // -- update UI & enable finish if there's no error + 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); + } + } + + /** + * 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 SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mBrowseButton) { + // Choose directory + DirectoryDialog dialog = new DirectoryDialog(getShell(), SWT.OPEN); + String path = mDir.getText().trim(); + if (path.length() > 0) { + dialog.setFilterPath(path); + } + String file = dialog.open(); + if (file != null) { + mDir.setText(file); + refresh(); + } + } else if (source == mSelectAllButton) { + mCheckboxTableViewer.setAllChecked(true); + mValues.importProjects = mProjectPaths; + } else if (source == mDeselectAllButton) { + mCheckboxTableViewer.setAllChecked(false); + mValues.importProjects = Collections.emptyList(); + } else if (source == mRefreshButton || source == mDir) { + refresh(); + } else if (source == mCopyCheckBox) { + mValues.copyIntoWorkspace = mCopyCheckBox.getSelection(); + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + // ---- KeyListener ---- + + @Override + public void keyPressed(KeyEvent e) { + if (e.getSource() == mDir) { + if (e.keyCode == SWT.CR) { + refresh(); + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + + // ---- TraverseListener ---- + + @Override + public void keyTraversed(TraverseEvent e) { + // Prevent Return from running through the wizard; return is handled by + // key listener to refresh project list instead + if (SWT.TRAVERSE_RETURN == e.detail) { + e.doit = false; + } + } + + // ---- Implements IStructuredContentProvider ---- + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return mProjectPaths != null ? mProjectPaths.toArray() : new Object[0]; + } + + // ---- Implements ICheckStateListener ---- + + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + // Try to disable other elements that conflict with this + Object[] checked = mCheckboxTableViewer.getCheckedElements(); + List<ImportedProject> selected = new ArrayList<ImportedProject>(checked.length); + for (Object o : checked) { + if (!mCheckboxTableViewer.getGrayed(o)) { + selected.add((ImportedProject) o); + } + } + mValues.importProjects = selected; + validatePage(); + + mCheckboxTableViewer.update(event.getElement(), null); + } + + // ---- Implements ControlListener ---- + + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + updateColumnWidths(); + } + + private final class ProjectCellLabelProvider extends CellLabelProvider { + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + int index = cell.getColumnIndex(); + ImportedProject project = (ImportedProject) element; + + Display display = mTable.getDisplay(); + Color fg; + if (mCheckboxTableViewer.getGrayed(element)) { + fg = display.getSystemColor(SWT.COLOR_DARK_GRAY); + } else { + fg = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); + } + cell.setForeground(fg); + cell.setBackground(display.getSystemColor(SWT.COLOR_LIST_BACKGROUND)); + + switch (index) { + case DIR_COLUMN: { + // Directory name + cell.setText(project.getRelativePath()); + return; + } + + case NAME_COLUMN: { + // New name + cell.setText(project.getProjectName()); + return; + } + default: + assert false : index; + } + cell.setText(""); + } + } + + /** Editing support for the project name column */ + private class ProjectNameEditingSupport extends EditingSupport { + private ProjectNameEditingSupport(ColumnViewer viewer) { + super(viewer); + } + + @Override + protected void setValue(Object element, Object value) { + ImportedProject project = (ImportedProject) element; + project.setProjectName(value.toString()); + mCheckboxTableViewer.update(element, null); + updateValidity(); + validatePage(); + } + + @Override + protected Object getValue(Object element) { + ImportedProject project = (ImportedProject) element; + return project.getProjectName(); + } + + @Override + protected CellEditor getCellEditor(Object element) { + return new TextCellEditor(mTable); + } + + @Override + protected boolean canEdit(Object element) { + return true; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.java new file mode 100644 index 000000000..1004fd692 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportProjectWizard.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.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; + +import java.io.File; + + +/** + * An "Import Android Project" wizard. + */ +public class ImportProjectWizard extends Wizard implements INewWizard { + private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ + + private NewProjectWizardState mValues; + private ImportPage mImportPage; + private IStructuredSelection mSelection; + + /** Constructs a new wizard default project wizard */ + public ImportProjectWizard() { + } + + @Override + public void addPages() { + mValues = new NewProjectWizardState(Mode.ANY); + mImportPage = new ImportPage(mValues); + if (mSelection != null) { + mImportPage.init(mSelection, AdtUtils.getActivePart()); + } + addPage(mImportPage); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mSelection = selection; + + setHelpAvailable(false); // TODO have help + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + + @Override + public boolean performFinish() { + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator + + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + AdtPlugin.displayError("Tools Out of Date?", + String.format("It looks like you do not have the latest version of the " + + "SDK Tools installed. Make sure you update via the SDK Manager " + + "first. (Could not find %1$s)", file.getPath())); + return false; + } + + NewProjectCreator creator = new NewProjectCreator(mValues, getContainer()); + if (!(creator.createAndroidProjects())) { + return false; + } + + // Open the default Java Perspective + OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); + action.run(); + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java new file mode 100644 index 000000000..74af651ca --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java @@ -0,0 +1,256 @@ +/* + * 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.newproject; + +import static com.android.SdkConstants.ATTR_NAME; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.AndroidManifestParser; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +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.sdk.Sdk; +import com.android.io.FolderWrapper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +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.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** An Android project to be imported */ +class ImportedProject { + private final File mLocation; + private String mActivityName; + private ManifestData mManifest; + private String mProjectName; + private String mRelativePath; + + ImportedProject(File location, String relativePath) { + super(); + mLocation = location; + mRelativePath = relativePath; + } + + File getLocation() { + return mLocation; + } + + String getRelativePath() { + return mRelativePath; + } + + @Nullable + ManifestData getManifest() { + if (mManifest == null) { + try { + mManifest = AndroidManifestParser.parse(new FolderWrapper(mLocation)); + } catch (SAXException e) { + // Some sort of error in the manifest file: report to the user in a better way? + AdtPlugin.log(e, null); + return null; + } catch (Exception e) { + AdtPlugin.log(e, null); + return null; + } + } + + return mManifest; + } + + @Nullable + public String getActivityName() { + if (mActivityName == null) { + // Compute the project name and the package name from the manifest + ManifestData manifest = getManifest(); + if (manifest != null) { + if (manifest.getLauncherActivity() != null) { + mActivityName = manifest.getLauncherActivity().getName(); + } + if (mActivityName == null || mActivityName.isEmpty()) { + Activity[] activities = manifest.getActivities(); + for (Activity activity : activities) { + mActivityName = activity.getName(); + if (mActivityName != null && !mActivityName.isEmpty()) { + break; + } + } + } + if (mActivityName != null) { + int index = mActivityName.lastIndexOf('.'); + mActivityName = mActivityName.substring(index + 1); + } + } + } + + return mActivityName; + } + + @NonNull + public String getProjectName() { + if (mProjectName == null) { + // Are we importing an Eclipse project? If so just use the existing project name + mProjectName = findEclipseProjectName(); + if (mProjectName != null) { + return mProjectName; + } + + String activityName = getActivityName(); + if (activityName == null || activityName.isEmpty()) { + // I could also look at the build files, say build.xml from ant, and + // try to glean the project name from there + mProjectName = mLocation.getName(); + } else { + // Try to derive it from the activity name: + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IStatus nameStatus = workspace.validateName(activityName, IResource.PROJECT); + if (nameStatus.isOK()) { + mProjectName = activityName; + } else { + // Try to derive it by escaping characters + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = activityName.length(); i < n; i++) { + char c = activityName.charAt(i); + if (c != IPath.DEVICE_SEPARATOR && c != IPath.SEPARATOR && c != '\\') { + sb.append(c); + } + } + if (sb.length() == 0) { + mProjectName = mLocation.getName(); + } else { + mProjectName = sb.toString(); + } + } + } + } + + return mProjectName; + } + + @Nullable + private String findEclipseProjectName() { + File projectFile = new File(mLocation, ".project"); //$NON-NLS-1$ + if (projectFile.exists()) { + String xml; + try { + xml = Files.toString(projectFile, Charsets.UTF_8); + Document doc = DomUtilities.parseDocument(xml, false); + if (doc != null) { + NodeList names = doc.getElementsByTagName(ATTR_NAME); + if (names.getLength() >= 1) { + Node nameElement = names.item(0); + String name = nameElement.getTextContent().trim(); + if (!name.isEmpty()) { + return name; + } + } + } + } catch (IOException e) { + // pass: don't attempt to read project name; must be some sort of unrelated + // file with the same name, perhaps from a different editor or IDE + } + } + + return null; + } + + public void setProjectName(@NonNull String newName) { + mProjectName = newName; + } + + public IAndroidTarget getTarget() { + // Pick a target: + // First try to find the one requested by project.properties + IAndroidTarget[] targets = Sdk.getCurrent().getTargets(); + ProjectProperties properties = ProjectProperties.load(mLocation.getPath(), + PropertyType.PROJECT); + if (properties != null) { + String targetProperty = properties.getProperty(ProjectProperties.PROPERTY_TARGET); + if (targetProperty != null) { + Matcher m = Pattern.compile("android-(.+)").matcher( //$NON-NLS-1$ + targetProperty.trim()); + if (m.matches()) { + String targetName = m.group(1); + int targetLevel; + try { + targetLevel = Integer.parseInt(targetName); + } catch (NumberFormatException nufe) { + // pass + targetLevel = -1; + } + for (IAndroidTarget t : targets) { + AndroidVersion version = t.getVersion(); + if (version.isPreview() && targetName.equals(version.getCodename())) { + return t; + } else if (targetLevel == version.getApiLevel()) { + return t; + } + } + if (targetLevel > 0) { + // If not found, pick the closest one that is higher than the + // api level + IAndroidTarget target = targets[targets.length - 1]; + int targetDelta = target.getVersion().getApiLevel() - targetLevel; + for (IAndroidTarget t : targets) { + int newDelta = t.getVersion().getApiLevel() - targetLevel; + if (newDelta >= 0 && newDelta < targetDelta) { + targetDelta = newDelta; + target = t; + } + } + + return target; + } + } + } + } + + // If not found, pick the closest one to the one requested by the + // project (in project.properties) that is still >= the minSdk version + IAndroidTarget target = targets[targets.length - 1]; + ManifestData manifest = getManifest(); + if (manifest != null) { + int minSdkLevel = manifest.getMinSdkVersion(); + int targetDelta = target.getVersion().getApiLevel() - minSdkLevel; + for (IAndroidTarget t : targets) { + int newDelta = t.getVersion().getApiLevel() - minSdkLevel; + if (newDelta >= 0 && newDelta < targetDelta) { + targetDelta = newDelta; + target = t; + } + } + } + + return target; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java new file mode 100644 index 000000000..d168c7503 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java @@ -0,0 +1,1520 @@ +/* + * Copyright (C) 2007 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.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROPERTIES; +import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_LIBRARY; + +import static org.eclipse.core.resources.IResource.DEPTH_ZERO; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.res2.ValueXmlHelper; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +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.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.AndroidNature; +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.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.io.StreamException; +import com.android.resources.Density; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; + +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.filesystem.IFileSystem; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceStatus; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRunnable; +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.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.WorkspaceModifyOperation; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * The actual project creator invoked from the New Project Wizard + * <p/> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + */ +public class NewProjectCreator { + + private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$ + private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$ + private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$ + private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$ + private static final String PARAM_IMPORT_RESOURCE_CLASS = "IMPORT_RESOURCE_CLASS"; //$NON-NLS-1$ + private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$ + private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$ + private static final String PARAM_SAMPLE_LOCATION = "SAMPLE_LOCATION"; //$NON-NLS-1$ + private static final String PARAM_SOURCE = "SOURCE"; //$NON-NLS-1$ + private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$ + private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$ + private static final String PARAM_IS_LIBRARY = "IS_LIBRARY"; //$NON-NLS-1$ + private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$ + // Warning: The expanded string PARAM_TEST_TARGET_PACKAGE must not contain the + // string "PACKAGE" since it collides with the replacement of PARAM_PACKAGE. + private static final String PARAM_TEST_TARGET_PACKAGE = "TEST_TARGET_PCKG"; //$NON-NLS-1$ + private static final String PARAM_TARGET_SELF = "TARGET_SELF"; //$NON-NLS-1$ + private static final String PARAM_TARGET_MAIN = "TARGET_MAIN"; //$NON-NLS-1$ + private static final String PARAM_TARGET_EXISTING = "TARGET_EXISTING"; //$NON-NLS-1$ + private static final String PARAM_REFERENCE_PROJECT = "REFERENCE_PROJECT"; //$NON-NLS-1$ + + private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$ + private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$ + private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$ + private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$ + private static final String PH_TEST_USES_LIBRARY = "TEST-USES-LIBRARY"; //$NON-NLS-1$ + private static final String PH_TEST_INSTRUMENTATION = "TEST-INSTRUMENTATION"; //$NON-NLS-1$ + + private static final String BIN_DIRECTORY = + SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP; + private static final String BIN_CLASSES_DIRECTORY = + SdkConstants.FD_OUTPUT + AdtConstants.WS_SEP + + SdkConstants.FD_CLASSES_OUTPUT + AdtConstants.WS_SEP; + private static final String RES_DIRECTORY = + SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP; + private static final String ASSETS_DIRECTORY = + SdkConstants.FD_ASSETS + AdtConstants.WS_SEP; + private static final String DRAWABLE_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + AdtConstants.WS_SEP; + private static final String DRAWABLE_XHDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.XHIGH.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_HDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.HIGH.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_MDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.MEDIUM.getResourceValue() + + AdtConstants.WS_SEP; + private static final String DRAWABLE_LDPI_DIRECTORY = + SdkConstants.FD_RES_DRAWABLE + '-' + Density.LOW.getResourceValue() + + AdtConstants.WS_SEP; + private static final String LAYOUT_DIRECTORY = + SdkConstants.FD_RES_LAYOUT + AdtConstants.WS_SEP; + private static final String VALUES_DIRECTORY = + SdkConstants.FD_RES_VALUES + AdtConstants.WS_SEP; + private static final String GEN_SRC_DIRECTORY = + SdkConstants.FD_GEN_SOURCES + AdtConstants.WS_SEP; + + private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$ + private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY + + "AndroidManifest.template"; //$NON-NLS-1$ + private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY + + "activity.template"; //$NON-NLS-1$ + private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY + + "uses-sdk.template"; //$NON-NLS-1$ + private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY + + "launcher_intent_filter.template"; //$NON-NLS-1$ + private static final String TEMPLATE_TEST_USES_LIBRARY = TEMPLATES_DIRECTORY + + "test_uses-library.template"; //$NON-NLS-1$ + private static final String TEMPLATE_TEST_INSTRUMENTATION = TEMPLATES_DIRECTORY + + "test_instrumentation.template"; //$NON-NLS-1$ + + + + private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY + + "strings.template"; //$NON-NLS-1$ + private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY + + "string.template"; //$NON-NLS-1$ + private static final String PROJECT_ICON = "ic_launcher.png"; //$NON-NLS-1$ + private static final String ICON_XHDPI = "ic_launcher_xhdpi.png"; //$NON-NLS-1$ + private static final String ICON_HDPI = "ic_launcher_hdpi.png"; //$NON-NLS-1$ + private static final String ICON_MDPI = "ic_launcher_mdpi.png"; //$NON-NLS-1$ + private static final String ICON_LDPI = "ic_launcher_ldpi.png"; //$NON-NLS-1$ + + private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$ + + private static final String STRING_RSRC_PREFIX = SdkConstants.STRING_PREFIX; + private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$ + private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$ + + private static final String[] DEFAULT_DIRECTORIES = new String[] { + BIN_DIRECTORY, BIN_CLASSES_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY }; + private static final String[] RES_DIRECTORIES = new String[] { + DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY }; + private static final String[] RES_DENSITY_ENABLED_DIRECTORIES = new String[] { + DRAWABLE_XHDPI_DIRECTORY, + DRAWABLE_HDPI_DIRECTORY, DRAWABLE_MDPI_DIRECTORY, DRAWABLE_LDPI_DIRECTORY, + LAYOUT_DIRECTORY, VALUES_DIRECTORY }; + + private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$ + private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$ + private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$ + + private final NewProjectWizardState mValues; + private final IRunnableContext mRunnableContext; + + /** + * Creates a new {@linkplain NewProjectCreator} + * @param values the wizard state with initial project parameters + * @param runnableContext the context to run project creation in + */ + public NewProjectCreator(NewProjectWizardState values, IRunnableContext runnableContext) { + mValues = values; + mRunnableContext = runnableContext; + } + + /** + * Before actually creating the project for a new project (as opposed to using an + * existing project), we check if the target location is a directory that either does + * not exist or is empty. + * + * If it's not empty, ask the user for confirmation. + * + * @param destination The destination folder where the new project is to be created. + * @return True if the destination doesn't exist yet or is an empty directory or is + * accepted by the user. + */ + private boolean validateNewProjectLocationIsEmpty(IPath destination) { + File f = new File(destination.toOSString()); + if (f.isDirectory() && f.list().length > 0) { + return AdtPlugin.displayPrompt("New Android Project", + "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?"); + } + return true; + } + + /** + * Structure that describes all the information needed to create a project. + * This is collected from the pages by {@link NewProjectCreator#createAndroidProjects()} + * and then used by + * {@link NewProjectCreator#createProjectAsync(IProgressMonitor, ProjectInfo, ProjectInfo)}. + */ + private static class ProjectInfo { + private final IProject mProject; + private final IProjectDescription mDescription; + private final Map<String, Object> mParameters; + private final HashMap<String, String> mDictionary; + + public ProjectInfo(IProject project, + IProjectDescription description, + Map<String, Object> parameters, + HashMap<String, String> dictionary) { + mProject = project; + mDescription = description; + mParameters = parameters; + mDictionary = dictionary; + } + + public IProject getProject() { + return mProject; + } + + public IProjectDescription getDescription() { + return mDescription; + } + + public Map<String, Object> getParameters() { + return mParameters; + } + + public HashMap<String, String> getDictionary() { + return mDictionary; + } + } + + /** + * Creates the android project. + * @return True if the project could be created. + */ + public boolean createAndroidProjects() { + if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) { + return importProjects(); + } + + final ProjectInfo mainData = collectMainPageInfo(); + final ProjectInfo testData = collectTestPageInfo(); + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, mainData, testData, null, true); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Creates the a plain Java project without typical android directories or an Android Nature. + * This is intended for use by unit tests and not as a general-purpose Java project creator. + * @return True if the project could be created. + */ + @VisibleForTesting + public boolean createJavaProjects() { + if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) { + return importProjects(); + } + + final ProjectInfo mainData = collectMainPageInfo(); + final ProjectInfo testData = collectTestPageInfo(); + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, mainData, testData, null, false); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Imports a list of projects + */ + private boolean importProjects() { + assert mValues.importProjects != null && !mValues.importProjects.isEmpty(); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + + final List<ProjectInfo> projectData = new ArrayList<ProjectInfo>(); + for (ImportedProject p : mValues.importProjects) { + + // Compute the project name and the package name from the manifest + ManifestData manifest = p.getManifest(); + if (manifest == null) { + continue; + } + String packageName = manifest.getPackage(); + String projectName = p.getProjectName(); + String minSdk = manifest.getMinSdkVersionString(); + + final IProject project = workspace.getRoot().getProject(projectName); + final IProjectDescription description = + workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_PROJECT, projectName); + parameters.put(PARAM_PACKAGE, packageName); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, Boolean.FALSE); + parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES); + + parameters.put(PARAM_SDK_TARGET, p.getTarget()); + + // TODO: Find out if these end up getting used in the import-path through the code! + parameters.put(PARAM_MIN_SDK_VERSION, minSdk); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.applicationName); + + if (mValues.copyIntoWorkspace) { + parameters.put(PARAM_SOURCE, p.getLocation()); + + // TODO: Make sure it isn't *already* in the workspace! + //IPath defaultLocation = Platform.getLocation(); + //if ((!mValues.useDefaultLocation || mValues.useExisting) + // && !defaultLocation.isPrefixOf(path)) { + //IPath workspaceLocation = Platform.getLocation().append(projectName); + //description.setLocation(workspaceLocation); + // DON'T SET THE LOCATION: It's IMPLIED and in fact it will generate + // an error if you set it! + } else { + // Create in place + description.setLocation(new Path(p.getLocation().getPath())); + } + + projectData.add(new ProjectInfo(project, description, parameters, dictionary)); + } + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, null, null, projectData, true); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Collects all the parameters needed to create the main project. + * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be + * created because parameters are incorrect or should not be created because there + * is no main page. + */ + private ProjectInfo collectMainPageInfo() { + if (mValues.mode == Mode.TEST) { + return null; + } + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProject project = workspace.getRoot().getProject(mValues.projectName); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_PROJECT, mValues.projectName); + parameters.put(PARAM_PACKAGE, mValues.packageName); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, mValues.mode == Mode.ANY && !mValues.useExisting); + parameters.put(PARAM_SAMPLE_LOCATION, mValues.chosenSample); + parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder); + parameters.put(PARAM_SDK_TARGET, mValues.target); + parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk); + + if (mValues.createActivity) { + parameters.put(PARAM_ACTIVITY, mValues.activityName); + } + + // create a dictionary of string that will contain name+content. + // we'll put all the strings into values/strings.xml + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.applicationName); + + IPath path = new Path(mValues.projectLocation.getPath()); + IPath defaultLocation = Platform.getLocation(); + if ((!mValues.useDefaultLocation || mValues.useExisting) + && !defaultLocation.isPrefixOf(path)) { + description.setLocation(path); + } + + if (mValues.mode == Mode.ANY && !mValues.useExisting && !mValues.useDefaultLocation && + !validateNewProjectLocationIsEmpty(path)) { + return null; + } + + return new ProjectInfo(project, description, parameters, dictionary); + } + + /** + * Collects all the parameters needed to create the test project. + * + * @return A new {@link ProjectInfo} on success. Returns null if the project cannot be + * created because parameters are incorrect or should not be created because there + * is no test page. + */ + private ProjectInfo collectTestPageInfo() { + if (mValues.mode != Mode.TEST && !mValues.createPairProject) { + return null; + } + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + String projectName = + mValues.mode == Mode.TEST ? mValues.projectName : mValues.testProjectName; + final IProject project = workspace.getRoot().getProject(projectName); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + + String pkg = + mValues.mode == Mode.TEST ? mValues.packageName : mValues.testPackageName; + + parameters.put(PARAM_PACKAGE, pkg); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, !mValues.useExisting); + parameters.put(PARAM_SRC_FOLDER, mValues.sourceFolder); + parameters.put(PARAM_SDK_TARGET, mValues.target); + parameters.put(PARAM_MIN_SDK_VERSION, mValues.minSdk); + + // Test-specific parameters + String testedPkg = mValues.createPairProject + ? mValues.packageName : mValues.testTargetPackageName; + if (testedPkg == null) { + assert mValues.testingSelf; + testedPkg = pkg; + } + + parameters.put(PARAM_TEST_TARGET_PACKAGE, testedPkg); + + if (mValues.testingSelf) { + parameters.put(PARAM_TARGET_SELF, true); + } else { + parameters.put(PARAM_TARGET_EXISTING, true); + parameters.put(PARAM_REFERENCE_PROJECT, mValues.testedProject); + } + + if (mValues.createPairProject) { + parameters.put(PARAM_TARGET_MAIN, true); + } + + // create a dictionary of string that will contain name+content. + // we'll put all the strings into values/strings.xml + final HashMap<String, String> dictionary = new HashMap<String, String>(); + dictionary.put(STRING_APP_NAME, mValues.testApplicationName); + + // Use the same logic to determine test project location as in + // ApplicationInfoPage#validateTestProjectLocation + IPath path = new Path(mValues.projectLocation.getPath()); + path = path.removeLastSegments(1).append(mValues.testProjectName); + IPath defaultLocation = Platform.getLocation(); + if ((!mValues.useDefaultLocation || mValues.useExisting) + && !path.equals(defaultLocation)) { + description.setLocation(path); + } + + if (!mValues.useExisting && !mValues.useDefaultLocation && + !validateNewProjectLocationIsEmpty(path)) { + return null; + } + + return new ProjectInfo(project, description, parameters, dictionary); + } + + /** + * Runs the operation in a different thread and display generated + * exceptions. + * + * @param op The asynchronous operation to run. + */ + private void runAsyncOperation(WorkspaceModifyOperation op) { + try { + mRunnableContext.run(true /* fork */, true /* cancelable */, op); + } catch (InvocationTargetException e) { + + AdtPlugin.log(e, "New Project Wizard failed"); + + // The runnable threw an exception + Throwable t = e.getTargetException(); + if (t instanceof CoreException) { + CoreException core = (CoreException) t; + if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { + // The error indicates the file system is not case sensitive + // and there's a resource with a similar name. + MessageDialog.openError(AdtPlugin.getShell(), + "Error", "Error: Case Variant Exists"); + } else { + ErrorDialog.openError(AdtPlugin.getShell(), + "Error", core.getMessage(), core.getStatus()); + } + } else { + // Some other kind of exception + String msg = t.getMessage(); + Throwable t1 = t; + while (msg == null && t1.getCause() != null) { + msg = t1.getMessage(); + t1 = t1.getCause(); + } + if (msg == null) { + msg = t.toString(); + } + MessageDialog.openError(AdtPlugin.getShell(), "Error", msg); + } + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Creates the actual project(s). This is run asynchronously in a different thread. + * + * @param monitor An existing monitor. + * @param mainData Data for main project. Can be null. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. + * @throws InvocationTargetException to wrap any unmanaged exception and + * return it to the calling thread. The method can fail if it fails + * to create or modify the project or if it is canceled by the user. + */ + private void createProjectAsync(IProgressMonitor monitor, + ProjectInfo mainData, + ProjectInfo testData, + List<ProjectInfo> importData, + boolean isAndroidProject) + throws InvocationTargetException { + monitor.beginTask("Create Android Project", 100); + try { + IProject mainProject = null; + + if (mainData != null) { + mainProject = createEclipseProject( + new SubProgressMonitor(monitor, 50), + mainData.getProject(), + mainData.getDescription(), + mainData.getParameters(), + mainData.getDictionary(), + null, + isAndroidProject); + + if (mainProject != null) { + final IJavaProject javaProject = JavaCore.create(mainProject); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + } + } + + if (testData != null) { + Map<String, Object> parameters = testData.getParameters(); + if (parameters.containsKey(PARAM_TARGET_MAIN) && mainProject != null) { + parameters.put(PARAM_REFERENCE_PROJECT, mainProject); + } + + IProject testProject = createEclipseProject( + new SubProgressMonitor(monitor, 50), + testData.getProject(), + testData.getDescription(), + parameters, + testData.getDictionary(), + null, + isAndroidProject); + if (testProject != null) { + final IJavaProject javaProject = JavaCore.create(testProject); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + } + } + + if (importData != null) { + for (final ProjectInfo data : importData) { + ProjectPopulator projectPopulator = null; + if (mValues.copyIntoWorkspace) { + projectPopulator = new ProjectPopulator() { + @Override + public void populate(IProject project) { + // Copy + IFileSystem fileSystem = EFS.getLocalFileSystem(); + File source = (File) data.getParameters().get(PARAM_SOURCE); + IFileStore sourceDir = new ReadWriteFileStore( + fileSystem.getStore(source.toURI())); + IFileStore destDir = new ReadWriteFileStore( + fileSystem.getStore(AdtUtils.getAbsolutePath(project))); + try { + sourceDir.copy(destDir, EFS.OVERWRITE, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + }; + } + IProject project = createEclipseProject( + new SubProgressMonitor(monitor, 50), + data.getProject(), + data.getDescription(), + data.getParameters(), + data.getDictionary(), + projectPopulator, + isAndroidProject); + if (project != null) { + final IJavaProject javaProject = JavaCore.create(project); + Display.getDefault().syncExec(new WorksetAdder(javaProject, + mValues.workingSets)); + ProjectHelper.enforcePreferredCompilerCompliance(javaProject); + } + } + } + } catch (CoreException e) { + throw new InvocationTargetException(e); + } catch (IOException e) { + throw new InvocationTargetException(e); + } catch (StreamException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + + /** Handler which can write contents into a project */ + public interface ProjectPopulator { + /** + * Add contents into the given project + * + * @param project the project to write into + * @throws InvocationTargetException if anything goes wrong + */ + public void populate(IProject project) throws InvocationTargetException; + } + + /** + * Creates the actual project, sets its nature and adds the required folders + * and files to it. This is run asynchronously in a different thread. + * + * @param monitor An existing monitor. + * @param project The project to create. + * @param description A description of the project. + * @param parameters Template parameters. + * @param dictionary String definition. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. + * @return The project newly created + * @throws StreamException + */ + private IProject createEclipseProject( + @NonNull IProgressMonitor monitor, + @NonNull IProject project, + @NonNull IProjectDescription description, + @NonNull Map<String, Object> parameters, + @Nullable Map<String, String> dictionary, + @Nullable ProjectPopulator projectPopulator, + boolean isAndroidProject) + throws CoreException, IOException, StreamException { + + // get the project target + IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET); + boolean legacy = isAndroidProject && target.getVersion().getApiLevel() < 4; + + // Create project and open it + project.create(description, new SubProgressMonitor(monitor, 10)); + if (monitor.isCanceled()) throw new OperationCanceledException(); + + project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10)); + + // Add the Java and android nature to the project + AndroidNature.setupProjectNatures(project, monitor, isAndroidProject); + + // Create folders in the project if they don't already exist + addDefaultDirectories(project, AdtConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); + String[] sourceFolders; + if (isAndroidProject) { + sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER), + GEN_SRC_DIRECTORY + }; + } else { + sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER) + }; + } + addDefaultDirectories(project, AdtConstants.WS_ROOT, sourceFolders, monitor); + + // Create the resource folders in the project if they don't already exist. + if (legacy) { + addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor); + } else { + addDefaultDirectories(project, RES_DIRECTORY, RES_DENSITY_ENABLED_DIRECTORIES, monitor); + } + + if (projectPopulator != null) { + try { + projectPopulator.populate(project); + } catch (InvocationTargetException ite) { + AdtPlugin.log(ite, null); + } + } + + // Setup class path: mark folders as source folders + IJavaProject javaProject = JavaCore.create(project); + setupSourceFolders(javaProject, sourceFolders, monitor); + + if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) { + // Create files in the project if they don't already exist + addManifest(project, parameters, dictionary, monitor); + + // add the default app icon + addIcon(project, legacy, monitor); + + // Create the default package components + addSampleCode(project, sourceFolders[0], parameters, dictionary, monitor); + + // add the string definition file if needed + if (dictionary != null && dictionary.size() > 0) { + addStringDictionaryFile(project, dictionary, monitor); + } + + // add the default proguard config + File libFolder = new File((String) parameters.get(PARAM_SDK_TOOLS_DIR), + SdkConstants.FD_LIB); + 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, + monitor); + + // Set output location + javaProject.setOutputLocation(project.getFolder(BIN_CLASSES_DIRECTORY).getFullPath(), + monitor); + } + + File sampleDir = (File) parameters.get(PARAM_SAMPLE_LOCATION); + if (sampleDir != null) { + // Copy project + copySampleCode(project, sampleDir, parameters, dictionary, monitor); + } + + // Create the reference to the target project + if (parameters.containsKey(PARAM_REFERENCE_PROJECT)) { + IProject refProject = (IProject) parameters.get(PARAM_REFERENCE_PROJECT); + if (refProject != null) { + IProjectDescription desc = project.getDescription(); + + // Add out reference to the existing project reference. + // We just created a project with no references so we don't need to expand + // the currently-empty current list. + desc.setReferencedProjects(new IProject[] { refProject }); + + project.setDescription(desc, IResource.KEEP_HISTORY, + new SubProgressMonitor(monitor, 10)); + + IClasspathEntry entry = JavaCore.newProjectEntry( + refProject.getFullPath(), //path + new IAccessRule[0], //accessRules + false, //combineAccessRules + new IClasspathAttribute[0], //extraAttributes + false //isExported + + ); + ProjectHelper.addEntryToClasspath(javaProject, entry); + } + } + + if (isAndroidProject) { + Sdk.getCurrent().initProject(project, target); + } + + // Fix the project to make sure all properties are as expected. + // Necessary for existing projects and good for new ones to. + ProjectHelper.fixProject(project); + + Boolean isLibraryProject = (Boolean) parameters.get(PARAM_IS_LIBRARY); + if (isLibraryProject != null && isLibraryProject.booleanValue() + && Sdk.getCurrent() != null && project.isOpen()) { + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + // make a working copy of the properties + ProjectPropertiesWorkingCopy properties = + state.getProperties().makeWorkingCopy(); + + properties.setProperty(PROPERTY_LIBRARY, Boolean.TRUE.toString()); + try { + properties.save(); + IResource projectProp = project.findMember(FN_PROJECT_PROPERTIES); + if (projectProp != null) { + projectProp.refreshLocal(DEPTH_ZERO, new NullProgressMonitor()); + } + } catch (Exception e) { + String msg = String.format( + "Failed to save %1$s for project %2$s", + SdkConstants.FN_PROJECT_PROPERTIES, project.getName()); + AdtPlugin.log(e, msg); + } + } + } + + return project; + } + + /** + * Creates a new project + * + * @param monitor An existing monitor. + * @param project The project to create. + * @param target the build target to associate with the project + * @param projectPopulator a handler for writing the template contents + * @param isLibrary whether this project should be marked as a library project + * @param projectLocation the location to write the project into + * @param workingSets Eclipse working sets, if any, to add the project to + * @throws CoreException if anything goes wrong + */ + public static void create( + @NonNull IProgressMonitor monitor, + @NonNull final IProject project, + @NonNull IAndroidTarget target, + @Nullable final ProjectPopulator projectPopulator, + boolean isLibrary, + @NonNull String projectLocation, + @NonNull final IWorkingSet[] workingSets) + throws CoreException { + final NewProjectCreator creator = new NewProjectCreator(null, null); + + final Map<String, String> dictionary = null; + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_SDK_TARGET, target); + parameters.put(PARAM_SRC_FOLDER, SdkConstants.FD_SOURCES); + parameters.put(PARAM_IS_NEW_PROJECT, false); + parameters.put(PARAM_SAMPLE_LOCATION, null); + parameters.put(PARAM_IS_LIBRARY, isLibrary); + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + if (projectLocation != null) { + IPath path = new Path(projectLocation); + IPath parent = new Path(path.toFile().getParent()); + IPath workspaceLocation = Platform.getLocation(); + if (!workspaceLocation.equals(parent)) { + description.setLocation(path); + } + } + + IWorkspaceRunnable workspaceRunnable = new IWorkspaceRunnable() { + @Override + public void run(IProgressMonitor submonitor) throws CoreException { + try { + creator.createEclipseProject(submonitor, project, description, parameters, + dictionary, projectPopulator, true); + } catch (IOException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Unexpected error while creating project", e)); + } catch (StreamException e) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Unexpected error while creating project", e)); + } + if (workingSets != null && workingSets.length > 0) { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + Display.getDefault().syncExec(new WorksetAdder(javaProject, + workingSets)); + } + } + } + }; + + ResourcesPlugin.getWorkspace().run(workspaceRunnable, monitor); + } + + /** + * Adds default directories to the project. + * + * @param project The Java Project to update. + * @param parentFolder The path of the parent folder. Must end with a + * separator. + * @param folders Folders to be added. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to create the directories in + * the project. + */ + private void addDefaultDirectories(IProject project, String parentFolder, + String[] folders, IProgressMonitor monitor) throws CoreException { + for (String name : folders) { + if (name.length() > 0) { + IFolder folder = project.getFolder(parentFolder + name); + if (!folder.exists()) { + folder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + } + } + + /** + * Adds the manifest to the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param dictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addManifest(IProject project, Map<String, Object> parameters, + Map<String, String> dictionary, IProgressMonitor monitor) + throws CoreException, IOException { + + // get IFile to the manifest and check if it's not already there. + IFile file = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML); + if (!file.exists()) { + + // Read manifest template + String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST); + + // Replace all keyword parameters + manifestTemplate = replaceParameters(manifestTemplate, parameters); + + if (manifestTemplate == null) { + // Inform the user there will be not manifest. + AdtPlugin.logAndPrintError(null, "Create Project" /*TAG*/, + "Failed to generate the Android manifest. Missing template %s", + TEMPLATE_MANIFEST); + // Abort now, there's no need to continue + return; + } + + if (parameters.containsKey(PARAM_ACTIVITY)) { + // now get the activity template + String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES); + + // If the activity name doesn't contain any dot, it's in the form + // "ClassName" and we need to expand it to ".ClassName" in the XML. + String name = (String) parameters.get(PARAM_ACTIVITY); + if (name.indexOf('.') == -1) { + // Duplicate the parameters map to avoid changing the caller + parameters = new HashMap<String, Object>(parameters); + parameters.put(PARAM_ACTIVITY, "." + name); //$NON-NLS-1$ + } + + // Replace all keyword parameters to make main activity. + String activities = replaceParameters(activityTemplate, parameters); + + // set the intent. + String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER); + + if (activities != null) { + if (intent != null) { + // set the intent to the main activity + activities = activities.replaceAll(PH_INTENT_FILTERS, intent); + } + + // set the activity(ies) in the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities); + } + } else { + // remove the activity(ies) from the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, ""); //$NON-NLS-1$ + } + + // Handle the case of the test projects + if (parameters.containsKey(PARAM_TEST_TARGET_PACKAGE)) { + // Set the uses-library needed by the test project + String usesLibrary = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_USES_LIBRARY); + if (usesLibrary != null) { + manifestTemplate = manifestTemplate.replaceAll( + PH_TEST_USES_LIBRARY, usesLibrary); + } + + // Set the instrumentation element needed by the test project + String instru = AdtPlugin.readEmbeddedTextFile(TEMPLATE_TEST_INSTRUMENTATION); + if (instru != null) { + manifestTemplate = manifestTemplate.replaceAll( + PH_TEST_INSTRUMENTATION, instru); + } + + // Replace PARAM_TEST_TARGET_PACKAGE itself now + manifestTemplate = replaceParameters(manifestTemplate, parameters); + + } else { + // remove the unused entries + manifestTemplate = manifestTemplate.replaceAll(PH_TEST_USES_LIBRARY, ""); //$NON-NLS-1$ + manifestTemplate = manifestTemplate.replaceAll(PH_TEST_INSTRUMENTATION, ""); //$NON-NLS-1$ + } + + String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION); + if (minSdkVersion != null && minSdkVersion.length() > 0) { + String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK); + if (usesSdkTemplate != null) { + String usesSdk = replaceParameters(usesSdkTemplate, parameters); + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk); + } + } else { + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, ""); + } + + // Reformat the file according to the user's formatting settings + manifestTemplate = reformat(XmlFormatStyle.MANIFEST, manifestTemplate); + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream( + manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the string resource file. + * + * @param project The Java Project to update. + * @param strings The list of strings to be added to the string file. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addStringDictionaryFile(IProject project, + Map<String, String> strings, IProgressMonitor monitor) + throws CoreException, IOException { + + // create the IFile object and check if the file doesn't already exist. + IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + VALUES_DIRECTORY + AdtConstants.WS_SEP + STRINGS_FILE); + if (!file.exists()) { + // get the Strings.xml template + String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS); + + // get the template for one string + String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING); + + // get all the string names + Set<String> stringNames = strings.keySet(); + + // loop on it and create the string definitions + StringBuilder stringNodes = new StringBuilder(); + for (String key : stringNames) { + // get the value from the key + String value = strings.get(key); + + // Escape values if necessary + value = ValueXmlHelper.escapeResourceString(value); + + // place them in the template + String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key); + stringDef = stringDef.replace(PARAM_STRING_CONTENT, value); + + // append to the other string + if (stringNodes.length() > 0) { + stringNodes.append('\n'); + } + stringNodes.append(stringDef); + } + + // put the string nodes in the Strings.xml template + stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS, + stringNodes.toString()); + + // reformat the file according to the user's formatting settings + stringDefinitionTemplate = reformat(XmlFormatStyle.RESOURCE, stringDefinitionTemplate); + + // write the file as UTF-8 + InputStream stream = new ByteArrayInputStream( + stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** Reformats the given contents with the current formatting settings */ + private String reformat(XmlFormatStyle style, String contents) { + if (AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); + return EclipseXmlPrettyPrinter.prettyPrint(contents, formatPrefs, style, + null /*lineSeparator*/); + } else { + return contents; + } + } + + /** + * Adds default application icon to the project. + * + * @param project The Java Project to update. + * @param legacy whether we're running in legacy mode (no density support) + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + */ + private void addIcon(IProject project, boolean legacy, IProgressMonitor monitor) + throws CoreException { + if (legacy) { // density support + // do medium density icon only, in the default drawable folder. + IFile file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); + } + } else { + // do all 4 icons. + IFile file; + + // extra high density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_XHDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_XHDPI), monitor); + } + + // high density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_HDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_HDPI), monitor); + } + + // medium density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_MDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_MDPI), monitor); + } + + // low density + file = project.getFile(RES_DIRECTORY + AdtConstants.WS_SEP + + DRAWABLE_LDPI_DIRECTORY + AdtConstants.WS_SEP + PROJECT_ICON); + if (!file.exists()) { + addFile(file, AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON_LDPI), monitor); + } + } + } + + /** + * Creates a file from a data source. + * @param dest the file to write + * @param source the content of the file. + * @param monitor the progress monitor + * @throws CoreException + */ + private void addFile(IFile dest, byte[] source, IProgressMonitor monitor) throws CoreException { + if (source != null) { + // Save in the project + InputStream stream = new ByteArrayInputStream(source); + dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Creates the package folder and copies the sample code in the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param dictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addSampleCode(IProject project, String sourceFolder, + Map<String, Object> parameters, Map<String, String> dictionary, + IProgressMonitor monitor) throws CoreException, IOException { + // create the java package directories. + IFolder pkgFolder = project.getFolder(sourceFolder); + String packageName = (String) parameters.get(PARAM_PACKAGE); + + // The PARAM_ACTIVITY key will be absent if no activity should be created, + // in which case activityName will be null. + String activityName = (String) parameters.get(PARAM_ACTIVITY); + + Map<String, Object> java_activity_parameters = new HashMap<String, Object>(parameters); + java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, ""); //$NON-NLS-1$ + + if (activityName != null) { + + String resourcePackageClass = null; + + // An activity name can be of the form ".package.Class", ".Class" or FQDN. + // The initial dot is ignored, as it is always added later in the templates. + int lastDotIndex = activityName.lastIndexOf('.'); + + if (lastDotIndex != -1) { + + // Resource class + if (lastDotIndex > 0) { + resourcePackageClass = packageName + '.' + SdkConstants.FN_RESOURCE_BASE; + } + + // Package name + if (activityName.startsWith(".")) { //$NON-NLS-1$ + packageName += activityName.substring(0, lastDotIndex); + } else { + packageName = activityName.substring(0, lastDotIndex); + } + + // Activity Class name + activityName = activityName.substring(lastDotIndex + 1); + } + + java_activity_parameters.put(PARAM_ACTIVITY, activityName); + java_activity_parameters.put(PARAM_PACKAGE, packageName); + if (resourcePackageClass != null) { + String importResourceClass = "\nimport " + resourcePackageClass + ";"; //$NON-NLS-1$ // $NON-NLS-2$ + java_activity_parameters.put(PARAM_IMPORT_RESOURCE_CLASS, importResourceClass); + } + } + + String[] components = packageName.split(AdtConstants.RE_DOT); + for (String component : components) { + pkgFolder = pkgFolder.getFolder(component); + if (!pkgFolder.exists()) { + pkgFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + + if (activityName != null) { + // create the main activity Java file + String activityJava = activityName + SdkConstants.DOT_JAVA; + IFile file = pkgFolder.getFile(activityJava); + if (!file.exists()) { + copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor, false); + } + + // create the layout file (if we're creating an + IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY); + file = layoutfolder.getFile(MAIN_LAYOUT_XML); + if (!file.exists()) { + copyFile(LAYOUT_TEMPLATE, file, parameters, monitor, true); + dictionary.put(STRING_HELLO_WORLD, String.format("Hello World, %1$s!", + activityName)); + } + } + } + + private void copySampleCode(IProject project, File sampleDir, + Map<String, Object> parameters, Map<String, String> dictionary, + IProgressMonitor monitor) throws CoreException { + // Copy the sampleDir into the project directory recursively + IFileSystem fileSystem = EFS.getLocalFileSystem(); + IFileStore sourceDir = new ReadWriteFileStore( + fileSystem.getStore(sampleDir.toURI())); + IFileStore destDir = new ReadWriteFileStore( + fileSystem.getStore(AdtUtils.getAbsolutePath(project))); + sourceDir.copy(destDir, EFS.OVERWRITE, null); + } + + /** + * In a sample we never duplicate source files as read-only. + * This creates a store that read files attributes and doesn't set the r-o flag. + */ + private static class ReadWriteFileStore extends FileStoreAdapter { + + public ReadWriteFileStore(IFileStore store) { + super(store); + } + + // Override when reading attributes + @Override + public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException { + IFileInfo info = super.fetchInfo(options, monitor); + info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false); + return info; + } + + // Override when writing attributes + @Override + public void putInfo(IFileInfo info, int options, IProgressMonitor storeMonitor) + throws CoreException { + info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, false); + super.putInfo(info, options, storeMonitor); + } + + @Deprecated + @Override + public IFileStore getChild(IPath path) { + IFileStore child = super.getChild(path); + if (!(child instanceof ReadWriteFileStore)) { + child = new ReadWriteFileStore(child); + } + return child; + } + + @Override + public IFileStore getChild(String name) { + return new ReadWriteFileStore(super.getChild(name)); + } + } + + /** + * Adds a file to the root of the project + * @param project the project to add the file to. + * @param destName the name to write the file as + * @param source the file to add. It'll keep the same filename once copied into the project. + * @param monitor the monitor to report progress to + * @throws FileNotFoundException if the file to be added does not exist + * @throws CoreException if writing the file does not work + */ + public static void addLocalFile(IProject project, File source, String destName, + IProgressMonitor monitor) throws FileNotFoundException, CoreException { + IFile dest = project.getFile(destName); + if (dest.exists() == false) { + FileInputStream stream = new FileInputStream(source); + dest.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the given folder to the project's class path. + * + * @param javaProject The Java Project to update. + * @param sourceFolders Template Parameters. + * @param monitor An existing monitor. + * @throws JavaModelException if the classpath could not be set. + */ + private void setupSourceFolders(IJavaProject javaProject, String[] sourceFolders, + IProgressMonitor monitor) throws JavaModelException { + IProject project = javaProject.getProject(); + + // get the list of entries. + IClasspathEntry[] entries = javaProject.getRawClasspath(); + + // remove the project as a source folder (This is the default) + entries = removeSourceClasspath(entries, project); + + // add the source folders. + for (String sourceFolder : sourceFolders) { + IFolder srcFolder = project.getFolder(sourceFolder); + + // remove it first in case. + entries = removeSourceClasspath(entries, srcFolder); + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newSourceEntry(srcFolder.getFullPath())); + } + + javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); + } + + + /** + * Removes the corresponding source folder from the class path entries if + * found. + * + * @param entries The class path entries to read. A copy will be returned. + * @param folder The parent source folder to remove. + * @return A new class path entries array. + */ + private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) { + if (folder == null) { + return entries; + } + IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath()); + int n = entries.length; + for (int i = n - 1; i >= 0; i--) { + if (entries[i].equals(source)) { + IClasspathEntry[] newEntries = new IClasspathEntry[n - 1]; + if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i); + if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1); + n--; + entries = newEntries; + } + } + return entries; + } + + + /** + * Copies the given file from our resource folder to the new project. + * Expects the file to the US-ASCII or UTF-8 encoded. + * + * @throws CoreException from IFile if failing to create the new file. + * @throws MalformedURLException from URL if failing to interpret the URL. + * @throws FileNotFoundException from RandomAccessFile. + * @throws IOException from RandomAccessFile.length() if can't determine the + * length. + */ + private void copyFile(String resourceFilename, IFile destFile, + Map<String, Object> parameters, IProgressMonitor monitor, boolean reformat) + throws CoreException, IOException { + + // Read existing file. + String template = AdtPlugin.readEmbeddedTextFile( + TEMPLATES_DIRECTORY + resourceFilename); + + // Replace all keyword parameters + template = replaceParameters(template, parameters); + + if (reformat) { + // Guess the formatting style based on the file location + XmlFormatStyle style = EclipseXmlPrettyPrinter + .getForFile(destFile.getProjectRelativePath()); + if (style != null) { + template = reformat(style, template); + } + } + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$ + destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + + /** + * Replaces placeholders found in a string with values. + * + * @param str the string to search for placeholders. + * @param parameters a map of <placeholder, Value> to search for in the string + * @return A new String object with the placeholder replaced by the values. + */ + private String replaceParameters(String str, Map<String, Object> parameters) { + + if (parameters == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null parameter map. String: '%s'", str); //$NON-NLS-1$ + return str; + } else if (str == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null template string"); //$NON-NLS-1$ + return str; + } + + for (Entry<String, Object> entry : parameters.entrySet()) { + if (entry != null && entry.getValue() instanceof String) { + Object value = entry.getValue(); + if (value == null) { + AdtPlugin.log(IStatus.ERROR, + "NPW replace parameters: null value for key '%s' in template '%s'", //$NON-NLS-1$ + entry.getKey(), + str); + } else { + str = str.replaceAll(entry.getKey(), (String) value); + } + } + } + + return str; + } + + private static class WorksetAdder implements Runnable { + private final IJavaProject mProject; + private final IWorkingSet[] mWorkingSets; + + private WorksetAdder(IJavaProject project, IWorkingSet[] workingSets) { + mProject = project; + mWorkingSets = workingSets; + } + + @Override + public void run() { + if (mWorkingSets.length > 0 && mProject != null + && mProject.exists()) { + PlatformUI.getWorkbench().getWorkingSetManager() + .addToWorkingSets(mProject, mWorkingSets); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java new file mode 100644 index 000000000..ff03b338f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizard.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +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.ui.INewWizard; +import org.eclipse.ui.IWorkbench; + +import java.io.File; + + +/** + * A "New Android Project" Wizard. + * <p/> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + * <p/> + * Do not derive from this class. + */ +public class NewProjectWizard extends Wizard implements INewWizard { + private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ + + private NewProjectWizardState mValues; + private ProjectNamePage mNamePage; + private SdkSelectionPage mSdkPage; + private SampleSelectionPage mSamplePage; + private ApplicationInfoPage mPropertiesPage; + private final Mode mMode; + private IStructuredSelection mSelection; + + /** Constructs a new wizard default project wizard */ + public NewProjectWizard() { + this(Mode.ANY); + } + + protected NewProjectWizard(Mode mode) { + mMode = mode; + switch (mMode) { + case SAMPLE: + setWindowTitle("New Android Sample Project"); + break; + case TEST: + setWindowTitle("New Android Test Project"); + break; + default: + setWindowTitle("New Android Project"); + break; + } + } + + @Override + public void addPages() { + mValues = new NewProjectWizardState(mMode); + + if (mMode != Mode.SAMPLE) { + mNamePage = new ProjectNamePage(mValues); + + if (mSelection != null) { + mNamePage.init(mSelection, AdtUtils.getActivePart()); + } + + addPage(mNamePage); + } + + if (mMode == Mode.TEST) { + addPage(new TestTargetPage(mValues)); + } + + mSdkPage = new SdkSelectionPage(mValues); + addPage(mSdkPage); + + if (mMode != Mode.TEST) { + // Sample projects can be created when entering the new/existing wizard, or + // the sample wizard + mSamplePage = new SampleSelectionPage(mValues); + addPage(mSamplePage); + } + + if (mMode != Mode.SAMPLE) { + // Project properties are entered in all project types except sample projects + mPropertiesPage = new ApplicationInfoPage(mValues); + addPage(mPropertiesPage); + } + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + mSelection = selection; + + setHelpAvailable(false); // TODO have help + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + + @Override + public boolean performFinish() { + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + File.separator + + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + AdtPlugin.displayError("Tools Out of Date?", + String.format("It looks like you do not have the latest version of the " + + "SDK Tools installed. Make sure you update via the SDK Manager " + + "first. (Could not find %1$s)", file.getPath())); + return false; + } + + NewProjectCreator creator = new NewProjectCreator(mValues, getContainer()); + if (!(creator.createAndroidProjects())) { + return false; + } + + // Open the default Java Perspective + OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); + action.run(); + return true; + } + + @Override + public IWizardPage getNextPage(IWizardPage page) { + if (page == mNamePage) { + // Skip the test target selection page unless creating a test project + if (mValues.mode != Mode.TEST) { + return mSdkPage; + } + } else if (page == mSdkPage) { + if (mValues.mode == Mode.SAMPLE) { + return mSamplePage; + } else if (mValues.mode != Mode.TEST) { + return mPropertiesPage; + } else { + // Done with wizard when creating from existing or creating test projects + return null; + } + } else if (page == mSamplePage) { + // Nothing more to be entered for samples + return null; + } + + return super.getNextPage(page); + } + + /** + * Returns the package name currently set by the wizard + * + * @return the current package name, or null + */ + public String getPackageName() { + return mValues.packageName; + } + + // TBD: Call setDialogSettings etc to store persistent state between wizard invocations. +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java new file mode 100644 index 000000000..06c0300b7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectWizardState.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link NewProjectWizardState} holds the state used by the various pages + * in the {@link NewProjectWizard} and its variations, and it can also be used + * to pass project information to the {@link NewProjectCreator}. + */ +public class NewProjectWizardState { + /** The mode to run the wizard in: creating test, or sample, or plain project */ + public Mode mode; + + /** + * If true, the project should be created from an existing codebase (pointed + * to by the {@link #projectLocation} or in the case of sample projects, the + * {@link #chosenSample}. Otherwise, create a brand new project from scratch. + */ + public boolean useExisting; + + /** + * Whether new projects should be created into the default project location + * (e.g. in the Eclipse workspace) or not + */ + public boolean useDefaultLocation = true; + + /** The build target SDK */ + public IAndroidTarget target; + /** True if the user has manually modified the target */ + public boolean targetModifiedByUser; + + /** The location to store projects into */ + public File projectLocation = new File(Platform.getLocation().toOSString()); + /** True if the project location name has been manually edited by the user */ + public boolean projectLocationModifiedByUser; + + /** The name of the project */ + public String projectName = ""; //$NON-NLS-1$ + /** True if the project name has been manually edited by the user */ + public boolean projectNameModifiedByUser; + + /** The application name */ + public String applicationName; + /** True if the application name has been manually edited by the user */ + public boolean applicationNameModifiedByUser; + + /** The package path */ + public String packageName; + /** True if the package name has been manually edited by the user */ + public boolean packageNameModifiedByUser; + + /** True if a new activity should be created */ + public boolean createActivity; + + /** The name of the new activity to be created */ + public String activityName; + /** True if the activity name has been manually edited by the user */ + public boolean activityNameModifiedByUser; + + /** The minimum SDK version to use with the project (may be null or blank) */ + public String minSdk; + /** True if the minimum SDK version has been manually edited by the user */ + public boolean minSdkModifiedByUser; + /** + * A list of paths to each of the available samples for the current SDK. + * The pair is (String: sample display name => File: sample directory). + * Note we want a list, not a map since we might have duplicates. + * */ + public List<Pair<String, File>> samples = new ArrayList<Pair<String, File>>(); + /** Path to the currently chosen sample */ + public File chosenSample; + + /** The name of the source folder, relative to the project root */ + public String sourceFolder = SdkConstants.FD_SOURCES; + /** The set of chosen working sets to use when creating the project */ + public IWorkingSet[] workingSets = new IWorkingSet[0]; + + /** + * A reference to a different project that the current test project will be + * testing. + */ + public IProject testedProject; + /** + * If true, this test project should be testing itself, otherwise it will be + * testing the project pointed to by {@link #testedProject}. + */ + public boolean testingSelf; + + // NOTE: These apply only to creating paired projects; when isTest is true + // we're using + // the normal fields above + /** + * If true, create a test project along with this plain project which will + * be testing the plain project. (This flag only applies when creating + * normal projects.) + */ + public boolean createPairProject; + /** + * The application name of the test application (only applies when + * {@link #createPairProject} is true) + */ + public String testApplicationName; + /** + * True if the testing application name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testApplicationNameModified; + /** + * The package name of the test application (only applies when + * {@link #createPairProject} is true) + */ + public String testPackageName; + /** + * True if the testing package name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testPackageModified; + /** + * The project name of the test project (only applies when + * {@link #createPairProject} is true) + */ + public String testProjectName; + /** + * True if the testing project name has been modified by the user (only + * applies when {@link #createPairProject} is true) + */ + public boolean testProjectModified; + /** Package name of the tested app */ + public String testTargetPackageName; + + /** + * Copy project into workspace? This flag only applies when importing + * projects (creating projects from existing source) + */ + public boolean copyIntoWorkspace; + + /** + * List of projects to be imported. Null if not importing projects. + */ + @Nullable + public List<ImportedProject> importProjects; + + /** + * Creates a new {@link NewProjectWizardState} + * + * @param mode the mode to run the wizard in + */ + public NewProjectWizardState(Mode mode) { + this.mode = mode; + if (mode == Mode.SAMPLE) { + useExisting = true; + } else if (mode == Mode.TEST) { + createActivity = false; + } + } + + /** + * Extract information (package name, application name, minimum SDK etc) from + * the given Android project. + * + * @param path the path to the project to extract information from + */ + public void extractFromAndroidManifest(Path path) { + String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); + if (!(new File(osPath).exists())) { + return; + } + + ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); + if (manifestData == null) { + return; + } + + String newPackageName = null; + Activity activity = null; + String newActivityName = null; + String minSdkVersion = null; + try { + newPackageName = manifestData.getPackage(); + minSdkVersion = manifestData.getMinSdkVersionString(); + + // try to get the first launcher activity. If none, just take the first activity. + activity = manifestData.getLauncherActivity(); + if (activity == null) { + Activity[] activities = manifestData.getActivities(); + if (activities != null && activities.length > 0) { + activity = activities[0]; + } + } + } catch (Exception e) { + // ignore exceptions + } + + if (newPackageName != null && newPackageName.length() > 0) { + packageName = newPackageName; + } + + if (activity != null) { + newActivityName = AndroidManifest.extractActivityName(activity.getName(), + newPackageName); + } + + if (newActivityName != null && newActivityName.length() > 0) { + activityName = newActivityName; + // we are "importing" an existing activity, not creating a new one + createActivity = false; + + // If project name and application names are empty, use the activity + // name as a default. If the activity name has dots, it's a part of a + // package specification and only the last identifier must be used. + if (newActivityName.indexOf('.') != -1) { + String[] ids = newActivityName.split(AdtConstants.RE_DOT); + newActivityName = ids[ids.length - 1]; + } + if (projectName == null || projectName.length() == 0 || + !projectNameModifiedByUser) { + projectName = newActivityName; + projectNameModifiedByUser = false; + } + if (applicationName == null || applicationName.length() == 0 || + !applicationNameModifiedByUser) { + applicationNameModifiedByUser = false; + applicationName = newActivityName; + } + } else { + activityName = ""; //$NON-NLS-1$ + + // There is no activity name to use to fill in the project and application + // name. However if there's a package name, we can use this as a base. + if (newPackageName != null && newPackageName.length() > 0) { + // Package name is a java identifier, so it's most suitable for + // an application name. + + if (applicationName == null || applicationName.length() == 0 || + !applicationNameModifiedByUser) { + applicationName = newPackageName; + } + + // For the project name, remove any dots + newPackageName = newPackageName.replace('.', '_'); + if (projectName == null || projectName.length() == 0 || + !projectNameModifiedByUser) { + projectName = newPackageName; + } + + } + } + + if (mode == Mode.ANY && useExisting) { + updateSdkTargetToMatchProject(path.toFile()); + } + + minSdk = minSdkVersion; + minSdkModifiedByUser = false; + } + + /** + * Try to find an SDK Target that matches the current MinSdkVersion. + * + * There can be multiple targets with the same sdk api version, so don't change + * it if it's already at the right version. Otherwise pick the first target + * that matches. + */ + public void updateSdkTargetToMatchMinSdkVersion() { + IAndroidTarget currentTarget = target; + if (currentTarget != null && currentTarget.getVersion().equals(minSdk)) { + return; + } + + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + IAndroidTarget[] targets = sdk.getTargets(); + for (IAndroidTarget t : targets) { + if (t.getVersion().equals(minSdk)) { + target = t; + return; + } + } + } + } + + /** + * Updates the SDK to reflect the SDK required by the project at the given + * location + * + * @param location the location of the project + */ + public void updateSdkTargetToMatchProject(File location) { + // Select the target matching the manifest's sdk or build properties, if any + IAndroidTarget foundTarget = null; + // This is the target currently in the UI + IAndroidTarget currentTarget = target; + String projectPath = location.getPath(); + + // If there's a current target defined, we do not allow to change it when + // operating in the create-from-sample mode -- since the available sample list + // is tied to the current target, so changing it would invalidate the project we're + // trying to load in the first place. + if (!targetModifiedByUser) { + ProjectProperties p = ProjectProperties.load(projectPath, + PropertyType.PROJECT); + if (p != null) { + String v = p.getProperty(ProjectProperties.PROPERTY_TARGET); + IAndroidTarget desiredTarget = Sdk.getCurrent().getTargetFromHashString(v); + // We can change the current target if: + // - we found a new desired target + // - there is no current target + // - or the current target can't run the desired target + if (desiredTarget != null && + (currentTarget == null || !desiredTarget.canRunOn(currentTarget))) { + foundTarget = desiredTarget; + } + } + + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = null; + if (sdk != null) { + targets = sdk.getTargets(); + } + if (targets == null) { + targets = new IAndroidTarget[0]; + } + + if (foundTarget == null && minSdk != null) { + // Otherwise try to match the requested min-sdk-version if we find an + // exact match, regardless of the currently selected target. + for (IAndroidTarget existingTarget : targets) { + if (existingTarget != null && + existingTarget.getVersion().equals(minSdk)) { + foundTarget = existingTarget; + break; + } + } + } + + if (foundTarget == null) { + // Or last attempt, try to match a sample project location and use it + // if we find an exact match, regardless of the currently selected target. + for (IAndroidTarget existingTarget : targets) { + if (existingTarget != null && + projectPath.startsWith(existingTarget.getLocation())) { + foundTarget = existingTarget; + break; + } + } + } + } + + if (foundTarget != null) { + target = foundTarget; + } + } + + /** + * Type of project being offered/created by the wizard + */ + public enum Mode { + /** Create a sample project. Testing options are not presented. */ + SAMPLE, + + /** + * Create a test project, either testing itself or some other project. + * Note that even if in the {@link #ANY} mode, a test project can be + * created as a *paired* project with the main project, so this flag + * only means that we are creating *just* a test project + */ + TEST, + + /** + * Create an Android project, which can be a plain project, optionally + * with a paired test project, or a sample project (the first page + * contains toggles for choosing which + */ + ANY; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java new file mode 100644 index 000000000..6b6a4c29e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewSampleProjectWizard.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +/** + * A "New Sample Android Project" Wizard. + * <p/> + * This displays the new project wizard pre-configured for samples only. + */ +public class NewSampleProjectWizard extends NewProjectWizard { + /** + * Creates a new wizard for creating a sample Android project + */ + public NewSampleProjectWizard() { + super(Mode.SAMPLE); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java new file mode 100644 index 000000000..e0959f4db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewTestProjectWizard.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +/** + * A "New Test Android Project" Wizard. + * <p/> + * This is really the {@link NewProjectWizard} that only displays the "test project" pages. + */ +public class NewTestProjectWizard extends NewProjectWizard { + /** + * Creates a new wizard for creating an Android Test Project + */ + public NewTestProjectWizard() { + super(Mode.TEST); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java new file mode 100644 index 000000000..d04ea897f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ProjectNamePage.java @@ -0,0 +1,606 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; +import static com.android.ide.eclipse.adt.AdtUtils.capitalize; +import static com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage.ACTIVITY_NAME_SUFFIX; +import static com.android.utils.SdkUtils.stripWhitespace; + +import com.android.SdkConstants; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.VersionCheck; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; + +import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +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.osgi.util.TextProcessor; +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 org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.io.File; +import java.net.URI; +import java.util.Locale; + +/** + * Initial page shown when creating projects which asks for the project name, + * the the location of the project, working sets, etc. + */ +public class ProjectNamePage extends WizardPage implements SelectionListener, ModifyListener { + private final NewProjectWizardState mValues; + /** Flag used when setting button/text state manually to ignore listener updates */ + private boolean mIgnore; + /** Last user-browsed location, static so that it be remembered for the whole session */ + private static String sCustomLocationOsPath = ""; //$NON-NLS-1$ + private static boolean sAutoComputeCustomLocation = true; + + private Text mProjectNameText; + private Text mLocationText; + private Button mCreateSampleRadioButton; + private Button mCreateNewButton; + private Button mUseDefaultCheckBox; + private Button mBrowseButton; + private Label mLocationLabel; + private WorkingSetGroup mWorkingSetGroup; + /** + * Whether we've made sure the Tools are up to date (enough that all the + * resources required by the New Project wizard are present -- we don't + * necessarily check for newer versions than that here; that's done by + * {@link VersionCheck}, though that check doesn't <b>enforce</b> an update + * since it needs to allow the user to proceed to access the SDK manager + * etc.) + */ + private boolean mCheckedSdkUptodate; + + /** + * Create the wizard. + * @param values current wizard state + */ + ProjectNamePage(NewProjectWizardState values) { + super("projectNamePage"); //$NON-NLS-1$ + mValues = values; + + setTitle("Create Android Project"); + setDescription("Select project name and type of project"); + mWorkingSetGroup = new WorkingSetGroup(); + setWorkingSets(new IWorkingSet[0]); + } + + void init(IStructuredSelection selection, IWorkbenchPart activePart) { + setWorkingSets(WorkingSetHelper.getSelectedWorkingSet(selection, activePart)); + } + + /** + * Create contents of the wizard. + * @param parent the parent to add the page to + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(3, false)); + + Label nameLabel = new Label(container, SWT.NONE); + nameLabel.setText("Project Name:"); + + mProjectNameText = new Text(container, SWT.BORDER); + mProjectNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + mProjectNameText.addModifyListener(this); + + if (mValues.mode != Mode.TEST) { + mCreateNewButton = new Button(container, SWT.RADIO); + mCreateNewButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mCreateNewButton.setText("Create new project in workspace"); + mCreateNewButton.addSelectionListener(this); + + // TBD: Should we hide this completely, and make samples something you only invoke + // from the "New Sample Project" wizard? + mCreateSampleRadioButton = new Button(container, SWT.RADIO); + mCreateSampleRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, + 3, 1)); + mCreateSampleRadioButton.setText("Create project from existing sample"); + mCreateSampleRadioButton.addSelectionListener(this); + } + + Label separator = new Label(container, SWT.SEPARATOR | SWT.HORIZONTAL); + separator.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); + + mUseDefaultCheckBox = new Button(container, SWT.CHECK); + mUseDefaultCheckBox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); + mUseDefaultCheckBox.setText("Use default location"); + mUseDefaultCheckBox.addSelectionListener(this); + + mLocationLabel = new Label(container, SWT.NONE); + mLocationLabel.setText("Location:"); + + mLocationText = new Text(container, SWT.BORDER); + mLocationText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mLocationText.addModifyListener(this); + + mBrowseButton = new Button(container, SWT.NONE); + mBrowseButton.setText("Browse..."); + mBrowseButton.addSelectionListener(this); + + Composite group = mWorkingSetGroup.createControl(container); + group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1)); + + setControl(container); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + try { + mIgnore = true; + if (mValues.projectName != null) { + mProjectNameText.setText(mValues.projectName); + mProjectNameText.setFocus(); + } + if (mValues.mode == Mode.ANY || mValues.mode == Mode.TEST) { + if (mValues.useExisting) { + assert false; // This is now handled by the separate import wizard + } else if (mCreateNewButton != null) { + mCreateNewButton.setSelection(true); + } + } else if (mValues.mode == Mode.SAMPLE) { + mCreateSampleRadioButton.setSelection(true); + } + if (mValues.projectLocation != null) { + mLocationText.setText(mValues.projectLocation.getPath()); + } + mUseDefaultCheckBox.setSelection(mValues.useDefaultLocation); + updateLocationState(); + } finally { + mIgnore = false; + } + } + + validatePage(); + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mProjectNameText) { + onProjectFieldModified(); + if (!mValues.useDefaultLocation && !mValues.projectLocationModifiedByUser) { + updateLocationPathField(null); + } + } else if (source == mLocationText) { + mValues.projectLocationModifiedByUser = true; + if (!mValues.useDefaultLocation) { + File f = new File(mLocationText.getText().trim()); + mValues.projectLocation = f; + if (f.exists() && f.isDirectory() && !f.equals(mValues.projectLocation)) { + updateLocationPathField(mValues.projectLocation.getPath()); + } + } + } + + validatePage(); + } + + private void onProjectFieldModified() { + mValues.projectName = mProjectNameText.getText().trim(); + mValues.projectNameModifiedByUser = true; + + if (!mValues.applicationNameModifiedByUser) { + mValues.applicationName = capitalize(mValues.projectName); + if (!mValues.testApplicationNameModified) { + mValues.testApplicationName = + ApplicationInfoPage.suggestTestApplicationName(mValues.applicationName); + } + } + if (!mValues.activityNameModifiedByUser) { + String name = capitalize(mValues.projectName); + mValues.activityName = stripWhitespace(name) + ACTIVITY_NAME_SUFFIX; + } + if (!mValues.testProjectModified) { + mValues.testProjectName = + ApplicationInfoPage.suggestTestProjectName(mValues.projectName); + } + if (!mValues.projectLocationModifiedByUser) { + updateLocationPathField(null); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + + if (source == mCreateNewButton && mCreateNewButton != null + && mCreateNewButton.getSelection()) { + mValues.useExisting = false; + if (mValues.mode == Mode.SAMPLE) { + // Only reset the mode if we're toggling from sample back to create new + // or create existing. We can only come to the sample state when we're in + // ANY mode. (In particular, we don't want to switch to ANY if you're + // in test mode. + mValues.mode = Mode.ANY; + } + updateLocationState(); + } else if (source == mCreateSampleRadioButton && mCreateSampleRadioButton.getSelection()) { + mValues.useExisting = true; + mValues.useDefaultLocation = true; + if (!mUseDefaultCheckBox.getSelection()) { + try { + mIgnore = true; + mUseDefaultCheckBox.setSelection(true); + } finally { + mIgnore = false; + } + } + mValues.mode = Mode.SAMPLE; + updateLocationState(); + } else if (source == mUseDefaultCheckBox) { + mValues.useDefaultLocation = mUseDefaultCheckBox.getSelection(); + updateLocationState(); + } else if (source == mBrowseButton) { + onOpenDirectoryBrowser(); + } + + validatePage(); + } + + /** + * Enables or disable the location widgets depending on the user selection: + * the location path is enabled when using the "existing source" mode (i.e. not new project) + * or in new project mode with the "use default location" turned off. + */ + private void updateLocationState() { + boolean isNewProject = !mValues.useExisting; + boolean isCreateFromSample = mValues.mode == Mode.SAMPLE; + boolean useDefault = mValues.useDefaultLocation && !isCreateFromSample; + boolean locationEnabled = (!isNewProject || !useDefault) && !isCreateFromSample; + + mUseDefaultCheckBox.setEnabled(isNewProject); + mLocationLabel.setEnabled(locationEnabled); + mLocationText.setEnabled(locationEnabled); + mBrowseButton.setEnabled(locationEnabled); + + updateLocationPathField(null); + } + + /** + * Display a directory browser and update the location path field with the selected path + */ + private void onOpenDirectoryBrowser() { + + String existingDir = mLocationText.getText().trim(); + + // Disable the path if it doesn't exist + if (existingDir.length() == 0) { + existingDir = null; + } else { + File f = new File(existingDir); + if (!f.exists()) { + existingDir = null; + } + } + + DirectoryDialog directoryDialog = new DirectoryDialog(mLocationText.getShell()); + directoryDialog.setMessage("Browse for folder"); + directoryDialog.setFilterPath(existingDir); + String dir = directoryDialog.open(); + + if (dir != null) { + updateLocationPathField(dir); + validatePage(); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + /** + * 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); + } + + /** + * Updates the location directory path field. + * <br/> + * When custom user selection is enabled, use the absDir argument if not null and also + * save it internally. If absDir is null, restore the last saved absDir. This allows the + * user selection to be remembered when the user switches from default to custom. + * <br/> + * When custom user selection is disabled, use the workspace default location with the + * current project name. This does not change the internally cached absDir. + * + * @param absDir A new absolute directory path or null to use the default. + */ + private void updateLocationPathField(String absDir) { + boolean isNewProject = !mValues.useExisting || mValues.mode == Mode.SAMPLE; + boolean useDefault = mValues.useDefaultLocation; + boolean customLocation = !isNewProject || !useDefault; + + if (!mIgnore) { + try { + mIgnore = true; + if (customLocation) { + if (absDir != null) { + // We get here if the user selected a directory with the "Browse" button. + // Disable auto-compute of the custom location unless the user selected + // the exact same path. + sAutoComputeCustomLocation = sAutoComputeCustomLocation && + absDir.equals(sCustomLocationOsPath); + sCustomLocationOsPath = TextProcessor.process(absDir); + } else if (sAutoComputeCustomLocation || + (!isNewProject && !new File(sCustomLocationOsPath).isDirectory())) { + // As a default import location, just suggest the home directory; the user + // needs to point to a project to import. + // TODO: Open file chooser automatically? + sCustomLocationOsPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + if (!mLocationText.getText().equals(sCustomLocationOsPath)) { + mLocationText.setText(sCustomLocationOsPath); + mValues.projectLocation = new File(sCustomLocationOsPath); + } + } else { + String value = Platform.getLocation().append(mValues.projectName).toString(); + value = TextProcessor.process(value); + if (!mLocationText.getText().equals(value)) { + mLocationText.setText(value); + mValues.projectLocation = new File(value); + } + } + } finally { + mIgnore = false; + } + } + + if (mValues.useExisting && mValues.projectLocation != null + && mValues.projectLocation.exists() && mValues.mode != Mode.SAMPLE) { + mValues.extractFromAndroidManifest(new Path(mValues.projectLocation.getPath())); + if (!mValues.projectNameModifiedByUser && mValues.projectName != null) { + try { + mIgnore = true; + mProjectNameText.setText(mValues.projectName); + } finally { + mIgnore = false; + } + } + } + } + + private void validatePage() { + IStatus status = null; + + // Validate project name -- unless we're creating a sample, in which case + // the user will get a chance to pick the name on the Sample page + if (mValues.mode != Mode.SAMPLE) { + status = validateProjectName(mValues.projectName); + } + + if (status == null || status.getSeverity() != IStatus.ERROR) { + IStatus validLocation = validateLocation(); + if (validLocation != null) { + status = validLocation; + } + } + + if (!mCheckedSdkUptodate) { + // Ensure that we have a recent enough version of the Tools that the right templates + // are available + File file = new File(AdtPlugin.getOsSdkFolder(), OS_SDK_TOOLS_LIB_FOLDER + + File.separator + FN_PROJECT_PROGUARD_FILE); + if (!file.exists()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("You do not have the latest version of the " + + "SDK Tools installed: Please update. (Missing %1$s)", file.getPath())); + } else { + mCheckedSdkUptodate = true; + } + } + + // -- update UI & enable finish if there's no error + 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 validateLocation() { + if (mValues.mode == Mode.SAMPLE) { + // Samples are always created in the default directory + return null; + } + + // Validate location + Path path = new Path(mValues.projectLocation.getPath()); + if (!mValues.useExisting) { + if (!mValues.useDefaultLocation) { + // If not using the default value validate the location. + URI uri = URIUtil.toURI(path.toOSString()); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject handle = workspace.getRoot().getProject(mValues.projectName); + IStatus locationStatus = workspace.validateProjectLocationURI(handle, uri); + if (!locationStatus.isOK()) { + return locationStatus; + } + // The location is valid as far as Eclipse is concerned (i.e. mostly not + // an existing workspace project.) Check it either doesn't exist or is + // a directory that is empty. + File f = path.toFile(); + if (f.exists() && !f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } else if (f.isDirectory()) { + // However if the directory exists, we should put a + // warning if it is not empty. We don't put an error + // (we'll ask the user again for confirmation before + // using the directory.) + String[] l = f.list(); + if (l != null && l.length != 0) { + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "The selected output directory is not empty."); + } + } + } else { + // Otherwise validate the path string is not empty + if (mValues.projectLocation.getPath().length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A directory name must be specified."); + } + File dest = path.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.", + mValues.projectName)); + } + } + } else { + // Must be an existing directory + File f = path.toFile(); + if (!f.isDirectory()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "An existing directory name must be specified."); + } + + // Check there's an android manifest in the directory + String osPath = path.append(SdkConstants.FN_ANDROID_MANIFEST_XML).toOSString(); + File manifestFile = new File(osPath); + if (!manifestFile.isFile()) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format( + "Choose a valid Android code directory\n" + + "(%1$s not found in %2$s.)", + SdkConstants.FN_ANDROID_MANIFEST_XML, f.getName())); + } + + // Parse it and check the important fields. + ManifestData manifestData = AndroidManifestHelper.parseForData(osPath); + if (manifestData == null) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("File %1$s could not be parsed.", osPath)); + } + String packageName = manifestData.getPackage(); + if (packageName == null || packageName.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No package name defined in %1$s.", osPath)); + } + Activity[] activities = manifestData.getActivities(); + if (activities == null || activities.length == 0) { + // This is acceptable now as long as no activity needs to be + // created + if (mValues.createActivity) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("No activity name defined in %1$s.", osPath)); + } + } + + // If there's already a .project, tell the user to use import instead. + if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ + return new Status(IStatus.WARNING, AdtPlugin.PLUGIN_ID, + "An Eclipse project already exists in this directory.\n" + + "Consider using File > Import > Existing Project instead."); + } + } + + return null; + } + + public static IStatus validateProjectName(String projectName) { + if (projectName == null || projectName.length() == 0) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Project name must be specified"); + } else { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IStatus nameStatus = workspace.validateName(projectName, IResource.PROJECT); + if (!nameStatus.isOK()) { + return nameStatus; + } else { + // Note: the case-sensitiveness of the project name matters and can cause a + // conflict *later* when creating the project resource, so let's check it now. + for (IProject existingProj : workspace.getRoot().getProjects()) { + if (projectName.equalsIgnoreCase(existingProj.getName())) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "A project with that name already exists in the workspace"); + } + } + } + } + + return null; + } + + @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(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java new file mode 100644 index 000000000..197247083 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SampleSelectionPage.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +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.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** Page where the user can select a sample to "instantiate" */ +class SampleSelectionPage extends WizardPage implements SelectionListener, ModifyListener { + private final NewProjectWizardState mValues; + private boolean mIgnore; + + private Table mTable; + private TableViewer mTableViewer; + private IAndroidTarget mCurrentSamplesTarget; + private Text mSampleProjectName; + + /** + * Create the wizard. + */ + SampleSelectionPage(NewProjectWizardState values) { + super("samplePage"); //$NON-NLS-1$ + setTitle("Select Sample"); + setDescription("Select which sample to create"); + mValues = values; + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + container.setLayout(new GridLayout(2, false)); + + mTableViewer = new TableViewer(container, SWT.BORDER | SWT.FULL_SELECTION); + mTable = mTableViewer.getTable(); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); + gridData.heightHint = 300; + mTable.setLayoutData(gridData); + mTable.addSelectionListener(this); + + setControl(container); + + Label projectLabel = new Label(container, SWT.NONE); + projectLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + projectLabel.setText("Project Name:"); + + mSampleProjectName = new Text(container, SWT.BORDER); + mSampleProjectName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mSampleProjectName.addModifyListener(this); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + + if (visible) { + if (mValues.projectName != null) { + try { + mIgnore = true; + mSampleProjectName.setText(mValues.projectName); + } finally { + mIgnore = false; + } + } + + // Update samples list if the SDK target has changed (or if it hasn't yet + // been populated) + if (mCurrentSamplesTarget != mValues.target) { + mCurrentSamplesTarget = mValues.target; + updateSamples(); + } + + validatePage(); + } + } + + private void updateSamples() { + IBaseLabelProvider labelProvider = new ColumnLabelProvider() { + @Override + public Image getImage(Object element) { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public String getText(Object element) { + if (element instanceof Pair<?, ?>) { + Object name = ((Pair<?, ?>) element).getFirst(); + return name.toString(); + } + return element.toString(); // Fallback. Should not happen. + } + }; + + mTableViewer.setContentProvider(new ArrayContentProvider()); + mTableViewer.setLabelProvider(labelProvider); + + if (mValues.samples != null && mValues.samples.size() > 0) { + Object[] samples = mValues.samples.toArray(); + mTableViewer.setInput(samples); + + mTable.select(0); + selectSample(mValues.samples.get(0).getSecond()); + extractNamesFromAndroidManifest(); + } + } + + private void selectSample(File sample) { + mValues.chosenSample = sample; + if (sample != null && !mValues.projectNameModifiedByUser) { + mValues.projectName = sample.getName(); + if (SdkConstants.FD_SAMPLE.equals(mValues.projectName) && + sample.getParentFile() != null) { + mValues.projectName = sample.getParentFile().getName() + '_' + mValues.projectName; + } + try { + mIgnore = true; + mSampleProjectName.setText(mValues.projectName); + } finally { + mIgnore = false; + } + updatedProjectName(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + if (e.getSource() == mTable) { + extractNamesFromAndroidManifest(); + int index = mTable.getSelectionIndex(); + if (index >= 0) { + Object[] roots = (Object[]) mTableViewer.getInput(); + selectSample(((Pair<String, File>) roots[index]).getSecond()); + } else { + selectSample(null); + } + } else { + assert false : e.getSource(); + } + + validatePage(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + @Override + public void modifyText(ModifyEvent e) { + if (mIgnore) { + return; + } + + if (e.getSource() == mSampleProjectName) { + mValues.projectName = mSampleProjectName.getText().trim(); + mValues.projectNameModifiedByUser = true; + updatedProjectName(); + } + + validatePage(); + } + + private void updatedProjectName() { + if (mValues.useDefaultLocation) { + mValues.projectLocation = Platform.getLocation().toFile(); + } + } + + /** + * A sample was selected. Update the location field, manifest and validate. + * Extract names from an android manifest. + * This is done only if the user selected the "use existing source" and a manifest xml file + * can actually be found in the custom user directory. + */ + private void extractNamesFromAndroidManifest() { + if (mValues.chosenSample == null || !mValues.chosenSample.isDirectory()) { + return; + } + + Path path = new Path(mValues.chosenSample.getPath()); + mValues.extractFromAndroidManifest(path); + } + + @Override + public boolean isPageComplete() { + if (mValues.mode != Mode.SAMPLE) { + return true; + } + + // Ensure that when creating samples, the Finish button isn't enabled until + // the user has reached and completed this page + if (mValues.chosenSample == null) { + return false; + } + + return super.isPageComplete(); + } + + private void validatePage() { + IStatus status = null; + if (mValues.samples == null || mValues.samples.size() == 0) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "The chosen SDK does not contain any samples"); + } else if (mValues.chosenSample == null) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Choose a sample"); + } else if (!mValues.chosenSample.exists()) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("Sample does not exist: %1$s", mValues.chosenSample.getPath())); + } else { + status = ProjectNamePage.validateProjectName(mValues.projectName); + } + + // -- update UI & enable finish if there's no error + 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); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java new file mode 100644 index 000000000..6cafcf057 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/SdkSelectionPage.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.ide.common.sdk.LoadStatus; +import com.android.ide.common.xml.AndroidManifestParser; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.io.FileWrapper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdkuilib.internal.widgets.SdkTargetSelector; +import com.android.utils.NullLogger; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; + +import java.io.File; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +/** A page in the New Project wizard where you select the target SDK */ +class SdkSelectionPage extends WizardPage implements ITargetChangeListener { + private final NewProjectWizardState mValues; + private boolean mIgnore; + private SdkTargetSelector mSdkTargetSelector; + + /** + * Create the wizard. + */ + SdkSelectionPage(NewProjectWizardState values) { + super("sdkSelection"); //$NON-NLS-1$ + mValues = values; + + setTitle("Select Build Target"); + AdtPlugin.getDefault().addTargetListener(this); + } + + @Override + public void dispose() { + AdtPlugin.getDefault().removeTargetListener(this); + super.dispose(); + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); + // Layout has 1 column + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + group.setText("Build Target"); + + // The selector is created without targets. They are added below in the change listener. + mSdkTargetSelector = new SdkTargetSelector(group, null); + + mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + mValues.target = mSdkTargetSelector.getSelected(); + mValues.targetModifiedByUser = true; + onSdkTargetModified(); + validatePage(); + } + }); + + onSdkLoaded(); + + setControl(group); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (mValues.mode == Mode.SAMPLE) { + setDescription("Choose an SDK to select a sample from"); + } else { + setDescription("Choose an SDK to target"); + } + try { + mIgnore = true; + if (mValues.target != null) { + mSdkTargetSelector.setSelection(mValues.target); + } + } finally { + mIgnore = false; + } + + validatePage(); + } + + @Override + public boolean isPageComplete() { + // Ensure that the Finish button isn't enabled until + // the user has reached and completed this page + if (mValues.target == null) { + return false; + } + + return super.isPageComplete(); + } + + /** + * Called when an SDK target is modified. + * + * Also changes the minSdkVersion field to reflect the sdk api level that has + * just been selected. + */ + private void onSdkTargetModified() { + if (mIgnore) { + return; + } + + IAndroidTarget target = mValues.target; + + // Update the minimum SDK text field? + // We do if one of two conditions are met: + if (target != null) { + boolean setMinSdk = false; + AndroidVersion version = target.getVersion(); + int apiLevel = version.getApiLevel(); + // 1. Has the user not manually edited the SDK field yet? If so, keep + // updating it to the selected value. + if (!mValues.minSdkModifiedByUser) { + setMinSdk = true; + } else { + // 2. Is the API level set to a higher level than the newly selected + // target SDK? If so, change it down to the new lower value. + String s = mValues.minSdk; + if (s.length() > 0) { + try { + int currentApi = Integer.parseInt(s); + if (currentApi > apiLevel) { + setMinSdk = true; + } + } catch (NumberFormatException nfe) { + // User may have typed something invalid -- ignore + } + } + } + if (setMinSdk) { + String minSdk; + if (version.isPreview()) { + minSdk = version.getCodename(); + } else { + minSdk = Integer.toString(apiLevel); + } + mValues.minSdk = minSdk; + } + } + + loadSamplesForTarget(target); + } + + /** + * Updates the list of all samples for the given target SDK. + * The list is stored in mSamplesPaths as absolute directory paths. + * The combo is recreated to match this. + */ + private void loadSamplesForTarget(IAndroidTarget target) { + // Keep the name of the old selection (if there were any samples) + File previouslyChosenSample = mValues.chosenSample; + + mValues.samples.clear(); + mValues.chosenSample = null; + + if (target != null) { + // Get the sample root path and recompute the list of samples + String samplesRootPath = target.getPath(IAndroidTarget.SAMPLES); + + File root = new File(samplesRootPath); + findSamplesManifests(root, root, null, null, mValues.samples); + + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + // Parse the extras to see if we can find samples that are + // compatible with the selected target API. + // First we need an SdkManager that suppresses all output. + SdkManager sdkman = sdk.getNewSdkManager(NullLogger.getLogger()); + + Map<File, String> extras = sdkman.getExtraSamples(); + for (Entry<File, String> entry : extras.entrySet()) { + File path = entry.getKey(); + String name = entry.getValue(); + + // Case where the sample is at the root of the directory and not + // in a per-sample sub-directory. + if (path.getName().equals(SdkConstants.FD_SAMPLE)) { + findSampleManifestInDir( + path, path, name, target.getVersion(), mValues.samples); + } + + // Scan sub-directories + findSamplesManifests( + path, path, name, target.getVersion(), mValues.samples); + } + } + + if (mValues.samples.isEmpty()) { + return; + } else { + Collections.sort(mValues.samples, new Comparator<Pair<String, File>>() { + @Override + public int compare(Pair<String, File> o1, Pair<String, File> o2) { + // Compare the display name of the sample + return o1.getFirst().compareTo(o2.getFirst()); + } + }); + } + + // Try to find the old selection. + if (previouslyChosenSample != null) { + String previouslyChosenName = previouslyChosenSample.getName(); + for (int i = 0, n = mValues.samples.size(); i < n; i++) { + File file = mValues.samples.get(i).getSecond(); + if (file.getName().equals(previouslyChosenName)) { + mValues.chosenSample = file; + break; + } + } + } + } + } + + /** + * Recursively find potential sample directories under the given directory. + * Actually lists any directory that contains an android manifest. + * Paths found are added the samplesPaths list. + * + * @param rootDir The "samples" root directory. Doesn't change during recursion. + * @param currDir The directory being scanned. Caller must initially set it to {@code rootDir}. + * @param extraName Optional name appended to the samples display name. Typically used to + * indicate a sample comes from a given extra package. + * @param targetVersion Optional target version filter. If non null, only samples that are + * compatible with the given target will be listed. + * @param samplesPaths A non-null list filled by this method with all samples found. The + * pair is (String: sample display name => File: sample directory). + */ + private void findSamplesManifests( + File rootDir, + File currDir, + @Nullable String extraName, + @Nullable AndroidVersion targetVersion, + List<Pair<String, File>> samplesPaths) { + if (!currDir.isDirectory()) { + return; + } + + for (File f : currDir.listFiles()) { + if (f.isDirectory()) { + findSampleManifestInDir(f, rootDir, extraName, targetVersion, samplesPaths); + + // Recurse in the project, to find embedded tests sub-projects + // We can however skip this recursion for known android sub-dirs that + // can't have projects, namely for sources, assets and resources. + String leaf = f.getName(); + if (!SdkConstants.FD_SOURCES.equals(leaf) && + !SdkConstants.FD_ASSETS.equals(leaf) && + !SdkConstants.FD_RES.equals(leaf)) { + findSamplesManifests(rootDir, f, extraName, targetVersion, samplesPaths); + } + } + } + } + + private void findSampleManifestInDir( + File sampleDir, + File rootDir, + String extraName, + AndroidVersion targetVersion, + List<Pair<String, File>> samplesPaths) { + // Assume this is a sample if it contains an android manifest. + File manifestFile = new File(sampleDir, SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestFile.isFile()) { + try { + ManifestData data = + AndroidManifestParser.parse(new FileWrapper(manifestFile)); + if (data != null) { + boolean accept = false; + if (targetVersion == null) { + accept = true; + } else if (targetVersion != null) { + int i = data.getMinSdkVersion(); + if (i != ManifestData.MIN_SDK_CODENAME) { + accept = i <= targetVersion.getApiLevel(); + } else { + String s = data.getMinSdkVersionString(); + if (s != null) { + accept = s.equals(targetVersion.getCodename()); + } + } + } + + if (accept) { + String name = getSampleDisplayName(extraName, rootDir, sampleDir); + samplesPaths.add(Pair.of(name, sampleDir)); + } + } + } catch (Exception e) { + // Ignore. Don't use a sample which manifest doesn't parse correctly. + AdtPlugin.log(IStatus.INFO, + "NPW ignoring malformed manifest %s", //$NON-NLS-1$ + manifestFile.getAbsolutePath()); + } + } + } + + /** + * Compute the sample name compared to its root directory. + */ + private String getSampleDisplayName(String extraName, File rootDir, File sampleDir) { + String name = null; + if (!rootDir.equals(sampleDir)) { + String path = sampleDir.getPath(); + int n = rootDir.getPath().length(); + if (path.length() > n) { + path = path.substring(n); + if (path.charAt(0) == File.separatorChar) { + path = path.substring(1); + } + if (path.endsWith(File.separator)) { + path = path.substring(0, path.length() - 1); + } + name = path.replaceAll(Pattern.quote(File.separator), " > "); //$NON-NLS-1$ + } + } + if (name == null && + rootDir.equals(sampleDir) && + sampleDir.getName().equals(SdkConstants.FD_SAMPLE) && + extraName != null) { + // This is an old-style extra with one single sample directory. Just use the + // extra's name as the same name. + return extraName; + } + if (name == null) { + // Otherwise try to use the sample's directory name as the sample name. + while (sampleDir != null && + (name == null || + SdkConstants.FD_SAMPLE.equals(name) || + SdkConstants.FD_SAMPLES.equals(name))) { + name = sampleDir.getName(); + sampleDir = sampleDir.getParentFile(); + } + } + if (name == null) { + if (extraName != null) { + // In the unlikely case nothing worked and we have an extra name, use that. + return extraName; + } else { + name = "Sample"; // fallback name... should not happen. //$NON-NLS-1$ + } + } + if (extraName != null) { + name = name + " [" + extraName + ']'; //$NON-NLS-1$ + } + + return name; + } + + private void validatePage() { + String error = null; + + if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADING) { + error = "The SDK is still loading; please wait."; + } + + if (error == null && mValues.target == null) { + error = "An SDK Target must be specified."; + } + + if (error == null && mValues.mode == Mode.SAMPLE) { + // Make sure this SDK target contains samples + if (mValues.samples == null || mValues.samples.size() == 0) { + error = "This target has no samples. Please select another target."; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + // ---- Implements ITargetChangeListener ---- + @Override + public void onSdkLoaded() { + if (mSdkTargetSelector == null) { + return; + } + + // Update the sdk target selector with the new targets + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + mSdkTargetSelector.setTargets(targets); + + // If there's only one target, select it. + // This will invoke the selection listener on the selector defined above. + if (targets != null && targets.length == 1) { + mValues.target = targets[0]; + mSdkTargetSelector.setSelection(mValues.target); + onSdkTargetModified(); + } else if (targets != null) { + // Pick the highest available platform by default (see issue #17505 + // for related discussion.) + IAndroidTarget initialTarget = null; + for (IAndroidTarget target : targets) { + if (target.isPlatform() + && !target.getVersion().isPreview() + && (initialTarget == null || + target.getVersion().getApiLevel() > + initialTarget.getVersion().getApiLevel())) { + initialTarget = target; + } + } + if (initialTarget != null) { + mValues.target = initialTarget; + try { + mIgnore = true; + mSdkTargetSelector.setSelection(mValues.target); + } finally { + mIgnore = false; + } + onSdkTargetModified(); + } + } + + validatePage(); + } + + @Override + public void onProjectTargetChange(IProject changedProject) { + // Ignore + } + + @Override + public void onTargetLoaded(IAndroidTarget target) { + // Ignore + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java new file mode 100644 index 000000000..f1c188ae9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/TestTargetPage.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ui.JavaElementLabelProvider; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ILabelProvider; +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.ui.dialogs.FilteredList; + +/** + * Page shown when creating a test project which lets you choose between testing + * yourself and testing a different project + */ +class TestTargetPage extends WizardPage implements SelectionListener { + private final NewProjectWizardState mValues; + /** Flag used when setting button/text state manually to ignore listener updates */ + private boolean mIgnore; + private String mLastExistingPackageName; + + private Button mCurrentRadioButton; + private Button mExistingRadioButton; + private FilteredList mProjectList; + private boolean mPageShown; + + /** + * Create the wizard. + */ + TestTargetPage(NewProjectWizardState values) { + super("testTargetPage"); //$NON-NLS-1$ + setTitle("Select Test Target"); + setDescription("Choose a project to test"); + mValues = values; + } + + /** + * Create contents of the wizard. + */ + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + + setControl(container); + container.setLayout(new GridLayout(2, false)); + + mCurrentRadioButton = new Button(container, SWT.RADIO); + mCurrentRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mCurrentRadioButton.setText("This project"); + mCurrentRadioButton.addSelectionListener(this); + + mExistingRadioButton = new Button(container, SWT.RADIO); + mExistingRadioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mExistingRadioButton.setText("An existing Android project:"); + mExistingRadioButton.addSelectionListener(this); + + ILabelProvider labelProvider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + mProjectList = new FilteredList(container, + SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE, labelProvider, + true /*ignoreCase*/, false /*allowDuplicates*/, true /* matchEmptyString*/); + mProjectList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + mProjectList.addSelectionListener(this); + } + + private void initializeList() { + ProjectChooserHelper helper = new ProjectChooserHelper(getShell(), null /*filter*/); + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + IJavaProject[] androidProjects = helper.getAndroidProjects(javaModel); + mProjectList.setElements(androidProjects); + if (mValues.testedProject != null) { + for (IJavaProject project : androidProjects) { + if (project.getProject() == mValues.testedProject) { + mProjectList.setSelection(new Object[] { project }); + break; + } + } + } else { + // No initial selection: force the user to choose + mProjectList.setSelection(new int[0]); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + mPageShown = true; + + if (visible) { + try { + mIgnore = true; + mCurrentRadioButton.setSelection(mValues.testingSelf); + mExistingRadioButton.setSelection(!mValues.testingSelf); + mProjectList.setEnabled(!mValues.testingSelf); + + if (mProjectList.isEmpty()) { + initializeList(); + } + if (!mValues.testingSelf) { + mProjectList.setFocus(); + IProject project = getSelectedProject(); + if (project != null) { + // The FilteredList seems to -insist- on selecting the first item + // in the list, even when the selection is explicitly set to an empty + // array. This means the user is looking at a selection, so we need + // to also go ahead and select this item in the model such that the + // two agree, even if we would have preferred to have no initial + // selection. + mValues.testedProject = project; + } + } + } finally { + mIgnore = false; + } + } + + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mExistingRadioButton) { + mProjectList.setEnabled(true); + mValues.testingSelf = false; + setExistingProject(getSelectedProject()); + mProjectList.setFocus(); + } else if (source == mCurrentRadioButton) { + mProjectList.setEnabled(false); + mValues.testingSelf = true; + mValues.testedProject = null; + } else { + // The event must be from the project list, which unfortunately doesn't + // pass itself as the selection event, it passes a reference to some internal + // table widget that it uses, so we check for this case last + IProject project = getSelectedProject(); + if (project != mValues.testedProject) { + setExistingProject(project); + } + } + + validatePage(); + } + + private IProject getSelectedProject() { + Object[] selection = mProjectList.getSelection(); + IProject project = selection != null && selection.length == 1 + ? ((IJavaProject) selection[0]).getProject() : null; + return project; + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + private void setExistingProject(IProject project) { + mValues.testedProject = project; + + // Try to update the application, package, sdk target and minSdkVersion accordingly + if (project != null && + (!mValues.applicationNameModifiedByUser || + !mValues.packageNameModifiedByUser || + !mValues.targetModifiedByUser || + !mValues.minSdkModifiedByUser)) { + ManifestData manifestData = AndroidManifestHelper.parseForData(project); + if (manifestData != null) { + String appName = String.format("%1$sTest", project.getName()); + String packageName = manifestData.getPackage(); + String minSdkVersion = manifestData.getMinSdkVersionString(); + IAndroidTarget sdkTarget = null; + if (Sdk.getCurrent() != null) { + sdkTarget = Sdk.getCurrent().getTarget(project); + } + + if (packageName == null) { + packageName = ""; //$NON-NLS-1$ + } + mLastExistingPackageName = packageName; + + if (!mValues.projectNameModifiedByUser) { + mValues.projectName = appName; + } + + if (!mValues.applicationNameModifiedByUser) { + mValues.applicationName = appName; + } + + if (!mValues.packageNameModifiedByUser) { + packageName += ".test"; //$NON-NLS-1$ + mValues.packageName = packageName; + } + + if (!mValues.targetModifiedByUser && sdkTarget != null) { + mValues.target = sdkTarget; + } + + if (!mValues.minSdkModifiedByUser) { + if (minSdkVersion != null || sdkTarget != null) { + mValues.minSdk = minSdkVersion; + } + if (sdkTarget == null) { + mValues.updateSdkTargetToMatchMinSdkVersion(); + } + } + } + } + + updateTestTargetPackageField(mLastExistingPackageName); + } + + /** + * Updates the test target package name + * + * When using the "self-test" option, the packageName argument is ignored and the + * current value from the project package is used. + * + * Otherwise the packageName is used if it is not null. + */ + private void updateTestTargetPackageField(String packageName) { + if (mValues.testingSelf) { + mValues.testTargetPackageName = mValues.packageName; + } else if (packageName != null) { + mValues.testTargetPackageName = packageName; + } + } + + @Override + public boolean isPageComplete() { + // Ensure that the user sees the page and makes a selection + if (!mPageShown) { + return false; + } + + return super.isPageComplete(); + } + + private void validatePage() { + String error = null; + + if (!mValues.testingSelf) { + if (mValues.testedProject == null) { + error = "Please select an existing Android project as a test target."; + } else if (mValues.projectName.equals(mValues.testedProject.getName())) { + error = "The main project name and the test project name must be different."; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else { + setErrorMessage(null); + setMessage(null); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java new file mode 100644 index 000000000..fb33a08b4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetGroup.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2000, 2009 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages; +import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.ui.IWorkingSet; +import org.eclipse.ui.dialogs.WorkingSetConfigurationBlock; + +/** + * Copied from + * org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne$WorkingSetGroup + * + * Creates the working set group with controls that allow + * the selection of working sets + */ +@SuppressWarnings("restriction") +public class WorkingSetGroup { + + private WorkingSetConfigurationBlock fWorkingSetBlock; + private Button mEnableButton; + + public WorkingSetGroup() { + String[] workingSetIds = new String[] { + IWorkingSetIDs.JAVA, IWorkingSetIDs.RESOURCE + }; + fWorkingSetBlock = new WorkingSetConfigurationBlock(workingSetIds, JavaPlugin.getDefault() + .getDialogSettings()); + } + + public Composite createControl(Composite composite) { + Group workingSetGroup = new Group(composite, SWT.NONE); + workingSetGroup.setFont(composite.getFont()); + workingSetGroup.setText(NewWizardMessages.NewJavaProjectWizardPageOne_WorkingSets_group); + workingSetGroup.setLayout(new GridLayout(1, false)); + + fWorkingSetBlock.createContent(workingSetGroup); + + // WorkingSetGroup is implemented in such a way that the checkbox it contains + // can only be programmatically set if there's an existing working set associated + // *before* we construct the control. However the control is created when the + // wizard is opened, not when the page is first shown. + // + // One choice is to duplicate the class in our project. + // Or find the checkbox we want and trigger it manually. + mEnableButton = findCheckbox(workingSetGroup); + + return workingSetGroup; + } + + public void setWorkingSets(IWorkingSet[] workingSets) { + fWorkingSetBlock.setWorkingSets(workingSets); + } + + public IWorkingSet[] getSelectedWorkingSets() { + try { + return fWorkingSetBlock.getSelectedWorkingSets(); + } catch (Throwable t) { + // Test scenarios; no UI is created, which the fWorkingSetBlock assumes + // (it dereferences the enabledButton) + return new IWorkingSet[0]; + } + } + + public boolean isChecked() { + return mEnableButton == null ? false : mEnableButton.getSelection(); + } + + public void setChecked(boolean state) { + if (mEnableButton != null) { + mEnableButton.setSelection(state); + } + } + + /** + * Finds the first button of style Checkbox in the given parent composite. + * Returns null if not found. + */ + private Button findCheckbox(Composite parent) { + for (Control control : parent.getChildren()) { + if (control instanceof Button && (control.getStyle() & SWT.CHECK) == SWT.CHECK) { + return (Button) control; + } else if (control instanceof Composite) { + Button found = findCheckbox((Composite) control); + if (found != null) { + return found; + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java new file mode 100755 index 000000000..428bfd331 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/WorkingSetHelper.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2000, 2009 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package com.android.ide.eclipse.adt.internal.wizards.newproject; + +import org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart; +import org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkingSet; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This class contains a helper method to deal with working sets. + * <p/> + * Copied from org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageOne + */ +@SuppressWarnings("restriction") +public final class WorkingSetHelper { + + private static final IWorkingSet[] EMPTY_WORKING_SET_ARRAY = new IWorkingSet[0]; + + /** This class is never instantiated. */ + private WorkingSetHelper() { + } + + public static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection, + IWorkbenchPart activePart) { + IWorkingSet[] selected= getSelectedWorkingSet(selection); + if (selected != null && selected.length > 0) { + for (int i= 0; i < selected.length; i++) { + if (!isValidWorkingSet(selected[i])) + return EMPTY_WORKING_SET_ARRAY; + } + return selected; + } + + if (!(activePart instanceof PackageExplorerPart)) + return EMPTY_WORKING_SET_ARRAY; + + PackageExplorerPart explorerPart= (PackageExplorerPart) activePart; + if (explorerPart.getRootMode() == PackageExplorerPart.PROJECTS_AS_ROOTS) { + //Get active filter + IWorkingSet filterWorkingSet= explorerPart.getFilterWorkingSet(); + if (filterWorkingSet == null) + return EMPTY_WORKING_SET_ARRAY; + + if (!isValidWorkingSet(filterWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + return new IWorkingSet[] {filterWorkingSet}; + } else { + //If we have been gone into a working set return the working set + Object input= explorerPart.getViewPartInput(); + if (!(input instanceof IWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + IWorkingSet workingSet= (IWorkingSet)input; + if (!isValidWorkingSet(workingSet)) + return EMPTY_WORKING_SET_ARRAY; + + return new IWorkingSet[] {workingSet}; + } + } + + private static IWorkingSet[] getSelectedWorkingSet(IStructuredSelection selection) { + if (!(selection instanceof ITreeSelection)) + return EMPTY_WORKING_SET_ARRAY; + + ITreeSelection treeSelection= (ITreeSelection) selection; + if (treeSelection.isEmpty()) + return EMPTY_WORKING_SET_ARRAY; + + List<?> elements = treeSelection.toList(); + if (elements.size() == 1) { + Object element= elements.get(0); + TreePath[] paths= treeSelection.getPathsFor(element); + if (paths.length != 1) + return EMPTY_WORKING_SET_ARRAY; + + TreePath path= paths[0]; + if (path.getSegmentCount() == 0) + return EMPTY_WORKING_SET_ARRAY; + + Object candidate= path.getSegment(0); + if (!(candidate instanceof IWorkingSet)) + return EMPTY_WORKING_SET_ARRAY; + + IWorkingSet workingSetCandidate= (IWorkingSet) candidate; + if (isValidWorkingSet(workingSetCandidate)) + return new IWorkingSet[] { workingSetCandidate }; + + return EMPTY_WORKING_SET_ARRAY; + } + + ArrayList<Object> result = new ArrayList<Object>(); + for (Iterator<?> iterator = elements.iterator(); iterator.hasNext();) { + Object element= iterator.next(); + if (element instanceof IWorkingSet && isValidWorkingSet((IWorkingSet) element)) { + result.add(element); + } + } + return result.toArray(new IWorkingSet[result.size()]); + } + + + private static boolean isValidWorkingSet(IWorkingSet workingSet) { + String id= workingSet.getId(); + if (!IWorkingSetIDs.JAVA.equals(id) && !IWorkingSetIDs.RESOURCE.equals(id)) + return false; + + if (workingSet.isAggregateWorkingSet()) + return false; + + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java new file mode 100644 index 000000000..0301b80fe --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java @@ -0,0 +1,653 @@ +/* + * 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.newxmlfile; + +import static com.android.SdkConstants.FD_RES; +import static com.android.SdkConstants.FD_RES_VALUES; +import static com.android.SdkConstants.RES_QUALIFIER_SEP; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.res2.ValueXmlHelper; +import com.android.ide.common.resources.LocaleManager; +import com.android.ide.common.resources.ResourceItem; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.FlagManager; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.resources.ResourceType; +import com.google.common.base.Charsets; +import com.google.common.collect.Maps; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +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.NullProgressMonitor; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +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.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; + +/** + * Dialog which adds a new translation to the project + */ +public class AddTranslationDialog extends Dialog implements ControlListener, SelectionListener, + TraverseListener { + private static final int KEY_COLUMN = 0; + private static final int DEFAULT_TRANSLATION_COLUMN = 1; + private static final int NEW_TRANSLATION_COLUMN = 2; + private final FolderConfiguration mConfiguration = new FolderConfiguration(); + private final IProject mProject; + private String mTarget; + private boolean mIgnore; + private Map<String, String> mTranslations; + private Set<String> mExistingLanguages; + private String mSelectedLanguage; + private String mSelectedRegion; + + private Table mTable; + private Combo mLanguageCombo; + private Combo mRegionCombo; + private ImageControl mFlag; + private Label mFile; + private Button mOkButton; + private Composite mErrorPanel; + private Label mErrorLabel; + private MyTableViewer mTableViewer; + + /** + * Creates the dialog. + * @param parentShell the parent shell + * @param project the project to add translations into + */ + public AddTranslationDialog(Shell parentShell, IProject project) { + super(parentShell); + setShellStyle(SWT.CLOSE | SWT.RESIZE | SWT.TITLE); + mProject = project; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout gl_container = new GridLayout(6, false); + gl_container.horizontalSpacing = 0; + container.setLayout(gl_container); + + Label languageLabel = new Label(container, SWT.NONE); + languageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + languageLabel.setText("Language:"); + mLanguageCombo = new Combo(container, SWT.READ_ONLY); + GridData gd_mLanguageCombo = new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1); + gd_mLanguageCombo.widthHint = 150; + mLanguageCombo.setLayoutData(gd_mLanguageCombo); + + Label regionLabel = new Label(container, SWT.NONE); + GridData gd_regionLabel = new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1); + gd_regionLabel.horizontalIndent = 10; + regionLabel.setLayoutData(gd_regionLabel); + regionLabel.setText("Region:"); + mRegionCombo = new Combo(container, SWT.READ_ONLY); + GridData gd_mRegionCombo = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1); + gd_mRegionCombo.widthHint = 150; + mRegionCombo.setLayoutData(gd_mRegionCombo); + mRegionCombo.setEnabled(false); + + mFlag = new ImageControl(container, SWT.NONE, null); + mFlag.setDisposeImage(false); + GridData gd_mFlag = new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1); + gd_mFlag.exclude = true; + gd_mFlag.widthHint = 32; + gd_mFlag.horizontalIndent = 3; + mFlag.setLayoutData(gd_mFlag); + + mFile = new Label(container, SWT.NONE); + mFile.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mTableViewer = new MyTableViewer(container, SWT.BORDER | SWT.FULL_SELECTION); + mTable = mTableViewer.getTable(); + mTable.setEnabled(false); + mTable.setLinesVisible(true); + mTable.setHeaderVisible(true); + mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 6, 2)); + mTable.addControlListener(this); + mTable.addTraverseListener(this); + // If you have difficulty opening up this form in WindowBuilder and it complains about + // the next line, change the type of the mTableViewer field and the above + // constructor call from MyTableViewer to TableViewer + TableViewerColumn keyViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE); + TableColumn keyColumn = keyViewerColumn.getColumn(); + keyColumn.setWidth(100); + keyColumn.setText("Key"); + TableViewerColumn defaultViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE); + TableColumn defaultColumn = defaultViewerColumn.getColumn(); + defaultColumn.setWidth(200); + defaultColumn.setText("Default"); + TableViewerColumn translationViewerColumn = new TableViewerColumn(mTableViewer, SWT.NONE); + TableColumn translationColumn = translationViewerColumn.getColumn(); + translationColumn.setWidth(200); + translationColumn.setText("New Translation"); + + mErrorPanel = new Composite(container, SWT.NONE); + GridData gd_mErrorLabel = new GridData(SWT.FILL, SWT.CENTER, false, false, 6, 1); + gd_mErrorLabel.exclude = true; + mErrorPanel.setLayoutData(gd_mErrorLabel); + + translationViewerColumn.setEditingSupport(new TranslationEditingSupport(mTableViewer)); + + fillLanguages(); + fillRegions(); + fillStrings(); + updateColumnWidths(); + validatePage(); + + mLanguageCombo.addSelectionListener(this); + mRegionCombo.addSelectionListener(this); + + return container; + } + + /** Populates the table with keys and default strings */ + private void fillStrings() { + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources resources = manager.getProjectResources(mProject); + mExistingLanguages = resources.getLanguages(); + + Collection<ResourceItem> items = resources.getResourceItemsOfType(ResourceType.STRING); + + ResourceItem[] array = items.toArray(new ResourceItem[items.size()]); + Arrays.sort(array); + + // TODO: Read in the actual XML files providing the default keys here + // (they can be obtained via ResourceItem.getSourceFileList()) + // such that we can read all the attributes associated with each + // item, and if it defines translatable=false, or the filename is + // donottranslate.xml, we can ignore it, and in other cases just + // duplicate all the attributes (such as "formatted=true", or other + // local conventions such as "product=tablet", or "msgid="123123123", + // etc.) + + mTranslations = Maps.newHashMapWithExpectedSize(items.size()); + IBaseLabelProvider labelProvider = new CellLabelProvider() { + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + int index = cell.getColumnIndex(); + ResourceItem item = (ResourceItem) element; + switch (index) { + case KEY_COLUMN: { + // Key + cell.setText(item.getName()); + return; + } + case DEFAULT_TRANSLATION_COLUMN: { + // Default translation + ResourceValue value = item.getResourceValue(ResourceType.STRING, + mConfiguration, false); + + if (value != null) { + cell.setText(value.getValue()); + return; + } + break; + } + case NEW_TRANSLATION_COLUMN: { + // New translation + String translation = mTranslations.get(item.getName()); + if (translation != null) { + cell.setText(translation); + return; + } + break; + } + default: + assert false : index; + } + cell.setText(""); + } + }; + + mTableViewer.setLabelProvider(labelProvider); + mTableViewer.setContentProvider(new ArrayContentProvider()); + mTableViewer.setInput(array); + } + + /** Populate the languages dropdown */ + private void fillLanguages() { + List<String> languageCodes = LocaleManager.getLanguageCodes(); + List<String> labels = new ArrayList<String>(); + for (String code : languageCodes) { + labels.add(code + ": " + LocaleManager.getLanguageName(code)); //$NON-NLS-1$ + } + Collections.sort(labels); + labels.add(0, "(Select)"); + mLanguageCombo.setItems(labels.toArray(new String[labels.size()])); + mLanguageCombo.select(0); + } + + /** Populate the regions dropdown */ + private void fillRegions() { + // TODO: When you switch languages, offer some "default" usable options. For example, + // when you choose English, offer the countries that use English, and so on. Unfortunately + // we don't have good data about this, we'd just need to hardcode a few common cases. + List<String> regionCodes = LocaleManager.getRegionCodes(); + List<String> labels = new ArrayList<String>(); + for (String code : regionCodes) { + labels.add(code + ": " + LocaleManager.getRegionName(code)); //$NON-NLS-1$ + } + Collections.sort(labels); + labels.add(0, "Any"); + mRegionCombo.setItems(labels.toArray(new String[labels.size()])); + mRegionCombo.select(0); + } + + /** React to resizing by distributing the space evenly between the last two columns */ + private void updateColumnWidths() { + Rectangle r = mTable.getClientArea(); + int availableWidth = r.width; + // Distribute all available space to the last two columns + int columnCount = mTable.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + TableColumn column = mTable.getColumn(i); + availableWidth -= column.getWidth(); + } + if (availableWidth != 0) { + TableColumn column = mTable.getColumn(DEFAULT_TRANSLATION_COLUMN); + column.setWidth(column.getWidth() + availableWidth / 2); + column = mTable.getColumn(NEW_TRANSLATION_COLUMN); + column.setWidth(column.getWidth() + availableWidth / 2 + availableWidth % 2); + } + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, + // Don't make the OK button default as in most dialogs, since when you press + // Return thinking you might edit a value it dismisses the dialog instead + false); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + mOkButton.setEnabled(false); + + validatePage(); + } + + /** + * Return the initial size of the dialog. + */ + @Override + protected Point getInitialSize() { + return new Point(800, 600); + } + + private void updateTarget() { + if (mSelectedLanguage == null) { + mTarget = null; + mFile.setText(""); + } else { + String folder = FD_RES + '/' + FD_RES_VALUES + RES_QUALIFIER_SEP + mSelectedLanguage; + if (mSelectedRegion != null) { + folder = folder + RES_QUALIFIER_SEP + 'r' + mSelectedRegion; + } + mTarget = folder + "/strings.xml"; //$NON-NLS-1$ + mFile.setText(String.format("Creating %1$s", mTarget)); + } + } + + private void updateFlag() { + if (mSelectedLanguage == null) { + // Nothing selected + ((GridData) mFlag.getLayoutData()).exclude = true; + } else { + FlagManager manager = FlagManager.get(); + Image flag = manager.getFlag(mSelectedLanguage, mSelectedRegion); + if (flag != null) { + ((GridData) mFlag.getLayoutData()).exclude = false; + mFlag.setImage(flag); + } + } + + mFlag.getParent().layout(true); + mFlag.getParent().redraw(); + } + + /** Actually create the new translation file and write it to disk */ + private void createTranslation() { + List<String> keys = new ArrayList<String>(mTranslations.keySet()); + Collections.sort(keys); + + StringBuilder sb = new StringBuilder(keys.size() * 120); + sb.append("<resources>\n\n"); //$NON-NLS-1$ + for (String key : keys) { + String value = mTranslations.get(key); + if (value == null || value.trim().isEmpty()) { + continue; + } + sb.append(" <string name=\""); //$NON-NLS-1$ + sb.append(key); + sb.append("\">"); //$NON-NLS-1$ + sb.append(ValueXmlHelper.escapeResourceString(value)); + sb.append("</string>\n"); //$NON-NLS-1$ + } + sb.append("\n</resources>"); //$NON-NLS-1$ + + IFile file = mProject.getFile(mTarget); + + try { + IContainer parent = file.getParent(); + AdtUtils.ensureExists(parent); + InputStream source = new ByteArrayInputStream(sb.toString().getBytes(Charsets.UTF_8)); + file.create(source, true, new NullProgressMonitor()); + AdtPlugin.openFile(file, null, true /*showEditorTab*/); + + // Ensure that the project resources updates itself to notice the new language. + // In theory, this shouldn't be necessary. + ResourceManager manager = ResourceManager.getInstance(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IFolder folder = root.getFolder(parent.getFullPath()); + manager.getResourceFolder(folder); + RenderPreviewManager.bumpRevision(); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + private void validatePage() { + if (mOkButton == null) { // Early initialization + return; + } + + String message = null; + + if (mSelectedLanguage == null) { + message = "Select a language"; + } else if (mExistingLanguages.contains(mSelectedLanguage)) { + if (mSelectedRegion == null) { + message = String.format("%1$s is already translated in this project", + LocaleManager.getLanguageName(mSelectedLanguage)); + } else { + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources resources = manager.getProjectResources(mProject); + SortedSet<String> regions = resources.getRegions(mSelectedLanguage); + if (regions.contains(mSelectedRegion)) { + message = String.format("%1$s (%2$s) is already translated in this project", + LocaleManager.getLanguageName(mSelectedLanguage), + LocaleManager.getRegionName(mSelectedRegion)); + } + } + } else { + // Require all strings to be translated? No, some of these may not + // be translatable (e.g. translatable=false, defined in donottranslate.xml, etc.) + //int missing = mTable.getItemCount() - mTranslations.values().size(); + //if (missing > 0) { + // message = String.format("Missing %1$d translations", missing); + //} + } + + boolean valid = message == null; + mTable.setEnabled(message == null); + mOkButton.setEnabled(valid); + showError(message); + } + + private void showError(String error) { + GridData data = (GridData) mErrorPanel.getLayoutData(); + + boolean show = error != null; + if (show == data.exclude) { + if (show) { + if (mErrorLabel == null) { + mErrorPanel.setLayout(new GridLayout(2, false)); + IWorkbench workbench = PlatformUI.getWorkbench(); + ISharedImages sharedImages = workbench.getSharedImages(); + String iconName = ISharedImages.IMG_OBJS_ERROR_TSK; + Image image = sharedImages.getImage(iconName); + @SuppressWarnings("unused") + ImageControl icon = new ImageControl(mErrorPanel, SWT.NONE, image); + + mErrorLabel = new Label(mErrorPanel, SWT.NONE); + mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, + 1, 1)); + } + mErrorLabel.setText(error); + } + data.exclude = !show; + mErrorPanel.getParent().layout(true); + } + } + + @Override + protected void okPressed() { + mTableViewer.applyEditorValue(); + + super.okPressed(); + createTranslation(); + } + + // ---- Implements ControlListener ---- + + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + if (mIgnore) { + return; + } + + updateColumnWidths(); + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (mIgnore) { + return; + } + + Object source = e.getSource(); + if (source == mLanguageCombo) { + try { + mIgnore = true; + mRegionCombo.select(0); + mSelectedRegion = null; + } finally { + mIgnore = false; + } + + int languageIndex = mLanguageCombo.getSelectionIndex(); + if (languageIndex == 0) { + mSelectedLanguage = null; + mRegionCombo.setEnabled(false); + } else { + // This depends on the label format + mSelectedLanguage = mLanguageCombo.getItem(languageIndex).substring(0, 2); + mRegionCombo.setEnabled(true); + } + + updateTarget(); + updateFlag(); + } else if (source == mRegionCombo) { + int regionIndex = mRegionCombo.getSelectionIndex(); + if (regionIndex == 0) { + mSelectedRegion = null; + } else { + mSelectedRegion = mRegionCombo.getItem(regionIndex).substring(0, 2); + } + + updateTarget(); + updateFlag(); + } + + try { + mIgnore = true; + validatePage(); + } finally { + mIgnore = false; + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + + // ---- TraverseListener ---- + + @Override + public void keyTraversed(TraverseEvent e) { + // If you press Return and we're not cell editing, start editing the current row + if (e.detail == SWT.TRAVERSE_RETURN && !mTableViewer.isCellEditorActive()) { + int index = mTable.getSelectionIndex(); + if (index != -1) { + Object next = mTable.getItem(index).getData(); + mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN); + } + } + } + + /** Editing support for the translation column */ + private class TranslationEditingSupport extends EditingSupport { + /** + * When true, setValue is being called as part of a default action + * (e.g. Return), not due to focus loss + */ + private boolean mDefaultAction; + + private TranslationEditingSupport(ColumnViewer viewer) { + super(viewer); + } + + @Override + protected void setValue(Object element, Object value) { + ResourceItem item = (ResourceItem) element; + mTranslations.put(item.getName(), value.toString()); + mTableViewer.update(element, null); + validatePage(); + + // If the user is pressing Return to finish editing a value (which is + // not the only way this method can get called - for example, if you click + // outside the cell while editing, the focus loss will also result in + // this method getting called), then mDefaultAction is true, and we automatically + // start editing the next row. + if (mDefaultAction) { + mTable.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + if (!mTable.isDisposed() && !mTableViewer.isCellEditorActive()) { + int index = mTable.getSelectionIndex(); + if (index != -1 && index < mTable.getItemCount() - 1) { + Object next = mTable.getItem(index + 1).getData(); + mTableViewer.editElement(next, NEW_TRANSLATION_COLUMN); + } + } + } + }); + } + } + + @Override + protected Object getValue(Object element) { + ResourceItem item = (ResourceItem) element; + String value = mTranslations.get(item.getName()); + if (value == null) { + return ""; + } + return value; + } + + @Override + protected CellEditor getCellEditor(Object element) { + return new TextCellEditor(mTable) { + @Override + protected void handleDefaultSelection(SelectionEvent event) { + try { + mDefaultAction = true; + super.handleDefaultSelection(event); + } finally { + mDefaultAction = false; + } + } + }; + } + + @Override + protected boolean canEdit(Object element) { + return true; + } + } + + private class MyTableViewer extends TableViewer { + public MyTableViewer(Composite parent, int style) { + super(parent, style); + } + + // Make this public so we can call it to ensure values are applied before the dialog + // is dismissed in {@link #okPressed} + @Override + public void applyEditorValue() { + super.applyEditorValue(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java new file mode 100644 index 000000000..1d6467e64 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; + +import com.android.SdkConstants; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector; +import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState; +import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode; +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo; +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard.Values; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.wizard.IWizardPage; +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.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** + * Second page of the {@link NewXmlFileWizard}. + * <p> + * This page is used for choosing the current configuration or specific resource + * folder. + */ +public class ChooseConfigurationPage extends WizardPage { + private Values mValues; + private Text mWsFolderPathTextField; + private ConfigurationSelector mConfigSelector; + private boolean mInternalWsFolderPathUpdate; + private boolean mInternalConfigSelectorUpdate; + + /** Absolute destination folder root, e.g. "/res/" */ + static final String RES_FOLDER_ABS = AdtConstants.WS_RESOURCES + AdtConstants.WS_SEP; + /** Relative destination folder root, e.g. "res/" */ + static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP; + + /** + * Create the wizard. + * + * @param values value object holding current wizard state + */ + public ChooseConfigurationPage(NewXmlFileWizard.Values values) { + super("chooseConfig"); + mValues = values; + setTitle("Choose Configuration Folder"); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + if (mValues.folderPath != null) { + mWsFolderPathTextField.setText(mValues.folderPath); + } + } + } + + @Override + public void createControl(Composite parent) { + // This UI is maintained with WindowBuilder. + + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayout(new GridLayout(2, false /* makeColumnsEqualWidth */)); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // label before configuration selector + Label label = new Label(composite, SWT.NONE); + label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + label.setText("Optional: Choose a specific configuration to limit the XML to:"); + + // configuration selector + mConfigSelector = new ConfigurationSelector(composite, SelectorMode.DEFAULT); + GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); + gd.verticalAlignment = SWT.FILL; + gd.horizontalAlignment = SWT.FILL; + gd.horizontalSpan = 2; + gd.heightHint = ConfigurationSelector.HEIGHT_HINT; + mConfigSelector.setLayoutData(gd); + mConfigSelector.setOnChangeListener(new ConfigurationChangeListener()); + + // Folder name: [text] + String tooltip = "The folder where the file will be generated, relative to the project."; + + Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData gdSeparator = new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1); + gdSeparator.heightHint = 10; + separator.setLayoutData(gdSeparator); + Label folderLabel = new Label(composite, SWT.NONE); + folderLabel.setText("Folder:"); + folderLabel.setToolTipText(tooltip); + + mWsFolderPathTextField = new Text(composite, SWT.BORDER); + mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mWsFolderPathTextField.setToolTipText(tooltip); + mWsFolderPathTextField.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + onWsFolderPathUpdated(); + } + }); + + setControl(composite); + + mConfigSelector.setConfiguration(mValues.configuration); + } + + /** + * Callback called when the Folder text field is changed, either programmatically + * or by the user. + */ + private void onWsFolderPathUpdated() { + if (mInternalWsFolderPathUpdate) { + return; + } + + String wsFolderPath = mWsFolderPathTextField.getText(); + + // This is a custom path, we need to sanitize it. + // First it should start with "/res/". Then we need to make sure there are no + // relative paths, things like "../" or "./" or even "//". + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + // We get "res/foo" from selections relative to the project when we want a "/res/foo" path. + if (wsFolderPath.startsWith(RES_FOLDER_REL)) { + wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length()); + + mInternalWsFolderPathUpdate = true; + mWsFolderPathTextField.setText(wsFolderPath); + mInternalWsFolderPathUpdate = false; + } + + mValues.folderPath = wsFolderPath; + + if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { + wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); + + int pos = wsFolderPath.indexOf(AdtConstants.WS_SEP_CHAR); + if (pos >= 0) { + wsFolderPath = wsFolderPath.substring(0, pos); + } + + String[] folderSegments = wsFolderPath.split(SdkConstants.RES_QUALIFIER_SEP); + + if (folderSegments.length > 0) { + String folderName = folderSegments[0]; + + // update config selector + mInternalConfigSelectorUpdate = true; + mConfigSelector.setConfiguration(folderSegments); + mInternalConfigSelectorUpdate = false; + + IWizardPage previous = ((NewXmlFileWizard) getWizard()).getPreviousPage(this); + if (previous instanceof NewXmlFileCreationPage) { + NewXmlFileCreationPage p = (NewXmlFileCreationPage) previous; + p.selectTypeFromFolder(folderName); + } + } + } + + validatePage(); + } + + /** + * Callback called when the configuration has changed in the {@link ConfigurationSelector}. + */ + private class ConfigurationChangeListener implements Runnable { + @Override + public void run() { + if (mInternalConfigSelectorUpdate) { + return; + } + + resetFolderPath(true /*validate*/); + } + } + + /** + * Reset the current Folder path based on the UI selection + * @param validate if true, force a call to {@link #validatePage()}. + */ + private void resetFolderPath(boolean validate) { + TypeInfo type = mValues.type; + if (type != null) { + mConfigSelector.getConfiguration(mValues.configuration); + StringBuilder sb = new StringBuilder(RES_FOLDER_ABS); + sb.append(mValues.configuration.getFolderName(type.getResFolderType())); + + mInternalWsFolderPathUpdate = true; + String newPath = sb.toString(); + mValues.folderPath = newPath; + mWsFolderPathTextField.setText(newPath); + mInternalWsFolderPathUpdate = false; + + if (validate) { + validatePage(); + } + } + } + + /** + * Returns the destination folder path relative to the project or an empty string. + * + * @return the currently edited folder + */ + public String getWsFolderPath() { + return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$ + } + + /** + * Validates the fields, displays errors and warnings. + * Enables the finish button if there are no errors. + */ + private void validatePage() { + String error = null; + String warning = null; + + // -- validate folder configuration + if (error == null) { + ConfigurationState state = mConfigSelector.getState(); + if (state == ConfigurationState.INVALID_CONFIG) { + ResourceQualifier qual = mConfigSelector.getInvalidQualifier(); + if (qual != null) { + error = + String.format("The qualifier '%1$s' is invalid in the folder configuration.", + qual.getName()); + } + } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) { + error = "The Region qualifier requires the Language qualifier."; + } + } + + // -- validate generated path + if (error == null) { + String wsFolderPath = getWsFolderPath(); + if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) { + error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS); + } + } + + // -- validate destination file doesn't exist + if (error == null) { + IFile file = mValues.getDestinationFile(); + if (file != null && file.exists()) { + warning = "The destination file already exists"; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else if (warning != null) { + setMessage(warning, IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java new file mode 100644 index 000000000..28fb8c032 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java @@ -0,0 +1,1163 @@ +/* + * Copyright (C) 2008 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.newxmlfile; + +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; +import static com.android.SdkConstants.LINEAR_LAYOUT; +import static com.android.SdkConstants.RES_QUALIFIER_SEP; +import static com.android.SdkConstants.SCROLL_VIEW; +import static com.android.SdkConstants.VALUE_FILL_PARENT; +import static com.android.SdkConstants.VALUE_MATCH_PARENT; +import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR; +import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS; + +import com.android.SdkConstants; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; +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.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; +import com.android.utils.SdkUtils; + +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.IAdaptable; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IBaseLabelProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +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.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.FileEditorInput; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +/** + * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create + * skeleton XML resources files for Android projects. + * <p/> + * This page is used to select the project, resource type and file name. + */ +class NewXmlFileCreationPage extends WizardPage { + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + // Ensure the initial focus is in the Name field; you usually don't need + // to edit the default text field (the project name) + if (visible && mFileNameTextField != null) { + mFileNameTextField.setFocus(); + } + + validatePage(); + } + + /** + * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) + */ + static class TypeInfo { + private final String mUiName; + private final ResourceFolderType mResFolderType; + private final String mTooltip; + private final Object mRootSeed; + private ArrayList<String> mRoots = new ArrayList<String>(); + private final String mXmlns; + private final String mDefaultAttrs; + private final String mDefaultRoot; + private final int mTargetApiLevel; + + public TypeInfo(String uiName, + String tooltip, + ResourceFolderType resFolderType, + Object rootSeed, + String defaultRoot, + String xmlns, + String defaultAttrs, + int targetApiLevel) { + mUiName = uiName; + mResFolderType = resFolderType; + mTooltip = tooltip; + mRootSeed = rootSeed; + mDefaultRoot = defaultRoot; + mXmlns = xmlns; + mDefaultAttrs = defaultAttrs; + mTargetApiLevel = targetApiLevel; + } + + /** Returns the UI name for the resource type. Unique. Never null. */ + String getUiName() { + return mUiName; + } + + /** Returns the tooltip for the resource type. Can be null. */ + String getTooltip() { + return mTooltip; + } + + /** + * Returns the name of the {@link ResourceFolderType}. + * Never null but not necessarily unique, + * e.g. two types use {@link ResourceFolderType#XML}. + */ + String getResFolderName() { + return mResFolderType.getName(); + } + + /** + * Returns the matching {@link ResourceFolderType}. + * Never null but not necessarily unique, + * e.g. two types use {@link ResourceFolderType#XML}. + */ + ResourceFolderType getResFolderType() { + return mResFolderType; + } + + /** + * Returns the seed used to fill the root element values. + * The seed might be either a String, a String array, an {@link ElementDescriptor}, + * a {@link DocumentDescriptor} or null. + */ + Object getRootSeed() { + return mRootSeed; + } + + /** + * Returns the default root element that should be selected by default. Can be + * null. + * + * @param project the associated project, or null if not known + */ + String getDefaultRoot(IProject project) { + return mDefaultRoot; + } + + /** + * Returns the list of all possible root elements for the resource type. + * This can be an empty ArrayList but not null. + * <p/> + * TODO: the root list SHOULD depend on the currently selected project, to include + * custom classes. + */ + ArrayList<String> getRoots() { + return mRoots; + } + + /** + * If the generated resource XML file requires an "android" XMLNS, this should be set + * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. + */ + String getXmlns() { + return mXmlns; + } + + /** + * When not null, this represent extra attributes that must be specified in the + * root element of the generated XML file. When null, no extra attributes are inserted. + * + * @param project the project to get the attributes for + * @param root the selected root element string, never null + */ + String getDefaultAttrs(IProject project, String root) { + return mDefaultAttrs; + } + + /** + * When not null, represents an extra string that should be written inside + * the element when constructed + * + * @param project the project to get the child content for + * @param root the chosen root element + * @return a string to be written inside the root element, or null if nothing + */ + String getChild(IProject project, String root) { + return null; + } + + /** + * The minimum API level required by the current SDK target to support this feature. + * + * @return the minimum API level + */ + public int getTargetApiLevel() { + return mTargetApiLevel; + } + } + + /** + * TypeInfo, information for each "type" of file that can be created. + */ + private static final TypeInfo[] sTypes = { + new TypeInfo( + "Layout", // UI name + "An XML file that describes a screen layout.", // tooltip + ResourceFolderType.LAYOUT, // folder type + AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed + LINEAR_LAYOUT, // default root + SdkConstants.NS_RESOURCES, // xmlns + "", // not used, see below + 1 // target API level + ) { + + @Override + String getDefaultRoot(IProject project) { + // TODO: Use GridLayout by default for new SDKs + // (when we've ironed out all the usability issues) + //Sdk currentSdk = Sdk.getCurrent(); + //if (project != null && currentSdk != null) { + // IAndroidTarget target = currentSdk.getTarget(project); + // // fill_parent was renamed match_parent in API level 8 + // if (target != null && target.getVersion().getApiLevel() >= 13) { + // return GRID_LAYOUT; + // } + //} + + return LINEAR_LAYOUT; + }; + + // The default attributes must be determined dynamically since whether + // we use match_parent or fill_parent depends on the API level of the + // project + @Override + String getDefaultAttrs(IProject project, String root) { + Sdk currentSdk = Sdk.getCurrent(); + String fill = VALUE_FILL_PARENT; + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(project); + // fill_parent was renamed match_parent in API level 8 + if (target != null && target.getVersion().getApiLevel() >= 8) { + fill = VALUE_MATCH_PARENT; + } + } + + // Only set "vertical" orientation of LinearLayouts by default; + // for GridLayouts for example we want to rely on the real default + // of the layout + String size = String.format( + "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ + + "android:layout_height=\"%2$s\"", //$NON-NLS-1$ + fill, fill); + if (LINEAR_LAYOUT.equals(root)) { + return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$ + } else { + return size; + } + } + + @Override + String getChild(IProject project, String root) { + // Create vertical linear layouts inside new scroll views + if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { + return " <LinearLayout " //$NON-NLS-1$ + + getDefaultAttrs(project, root).replace('\n', ' ') + + " android:orientation=\"vertical\"" //$NON-NLS-1$ + + "></LinearLayout>\n"; //$NON-NLS-1$ + } + return null; + } + }, + new TypeInfo("Values", // UI name + "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip + ResourceFolderType.VALUES, // folder type + SdkConstants.TAG_RESOURCES, // root seed + null, // default root + null, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Drawable", // UI name + "An XML file that describes a drawable.", // tooltip + ResourceFolderType.DRAWABLE, // folder type + AndroidTargetData.DESCRIPTOR_DRAWABLE, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Menu", // UI name + "An XML file that describes an menu.", // tooltip + ResourceFolderType.MENU, // folder type + SdkConstants.TAG_MENU, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Color List", // UI name + "An XML file that describes a color state list.", // tooltip + ResourceFolderType.COLOR, // folder type + AndroidTargetData.DESCRIPTOR_COLOR, // root seed + "selector", //$NON-NLS-1$ // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Property Animation", // UI name + "An XML file that describes a property animation", // tooltip + ResourceFolderType.ANIMATOR, // folder type + AndroidTargetData.DESCRIPTOR_ANIMATOR, // root seed + "set", //$NON-NLS-1$ // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 11 // target API level + ), + new TypeInfo("Tween Animation", // UI name + "An XML file that describes a tween animation.", // tooltip + ResourceFolderType.ANIM, // folder type + AndroidTargetData.DESCRIPTOR_ANIM, // root seed + "set", //$NON-NLS-1$ // default root + null, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("AppWidget Provider", // UI name + "An XML file that describes a widget provider.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 3 // target API level + ), + new TypeInfo("Preference", // UI name + "An XML file that describes preferences.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed + SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Searchable", // UI name + "An XML file that describes a searchable.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in + // this menu since it's not often used for creating XML files. + }; + + private NewXmlFileWizard.Values mValues; + private ProjectCombo mProjectButton; + private Text mFileNameTextField; + private Combo mTypeCombo; + private IStructuredSelection mInitialSelection; + private ResourceFolderType mInitialFolderType; + private boolean mInternalTypeUpdate; + private TargetChangeListener mSdkTargetChangeListener; + private Table mRootTable; + private TableViewer mRootTableViewer; + + // --- UI creation --- + + /** + * Constructs a new {@link NewXmlFileCreationPage}. + * <p/> + * Called by {@link NewXmlFileWizard#createMainPage}. + */ + protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { + super(pageName); + mValues = values; + setPageComplete(false); + } + + public void setInitialSelection(IStructuredSelection initialSelection) { + mInitialSelection = initialSelection; + } + + public void setInitialFolderType(ResourceFolderType initialType) { + mInitialFolderType = initialType; + } + + /** + * Called by the parent Wizard to create the UI for this Wizard Page. + * + * {@inheritDoc} + * + * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) + */ + @Override + @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused + public void createControl(Composite parent) { + // This UI is maintained with WindowBuilder. + + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/)); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // label before type radios + Label typeLabel = new Label(composite, SWT.NONE); + typeLabel.setText("Resource Type:"); + + mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY); + mTypeCombo.setToolTipText("What type of resource would you like to create?"); + mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (mInitialFolderType != null) { + mTypeCombo.setEnabled(false); + } + mTypeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + TypeInfo type = getSelectedType(); + if (type != null) { + onSelectType(type); + } + } + }); + + // separator + Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL); + gd2.horizontalAlignment = SWT.FILL; + gd2.horizontalSpan = 2; + separator.setLayoutData(gd2); + + // Project: [button] + String tooltip = "The Android Project where the new resource file will be created."; + Label projectLabel = new Label(composite, SWT.NONE); + projectLabel.setText("Project:"); + projectLabel.setToolTipText(tooltip); + + ProjectChooserHelper helper = + new ProjectChooserHelper(getShell(), null /* filter */); + + mProjectButton = new ProjectCombo(helper, composite, mValues.project); + mProjectButton.setToolTipText(tooltip); + mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mProjectButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IProject project = mProjectButton.getSelectedProject(); + if (project != mValues.project) { + changeProject(project); + } + }; + }); + + // Filename: [text] + Label fileLabel = new Label(composite, SWT.NONE); + fileLabel.setText("File:"); + fileLabel.setToolTipText("The name of the resource file to create."); + + mFileNameTextField = new Text(composite, SWT.BORDER); + mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFileNameTextField.setToolTipText(tooltip); + mFileNameTextField.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mValues.name = mFileNameTextField.getText(); + validatePage(); + } + }); + + // separator + Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData gd = new GridData(GridData.GRAB_HORIZONTAL); + gd.horizontalAlignment = SWT.FILL; + gd.horizontalSpan = 2; + rootSeparator.setLayoutData(gd); + + // Root Element: + // [TableViewer] + Label rootLabel = new Label(composite, SWT.NONE); + rootLabel.setText("Root Element:"); + new Label(composite, SWT.NONE); + + mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION); + mRootTable = mRootTableViewer.getTable(); + GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); + tableGridData.heightHint = 200; + mRootTable.setLayoutData(tableGridData); + + setControl(composite); + + // Update state the first time + setErrorMessage(null); + setMessage(null); + + initializeFromSelection(mInitialSelection); + updateAvailableTypes(); + initializeFromFixedType(); + initializeRootValues(); + installTargetChangeListener(); + + initialSelectType(); + validatePage(); + } + + private void initialSelectType() { + TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); + int typeIndex = getTypeComboIndex(mValues.type); + if (typeIndex == -1) { + typeIndex = 0; + } else { + assert mValues.type == types[typeIndex]; + } + mTypeCombo.select(typeIndex); + onSelectType(types[typeIndex]); + updateRootCombo(types[typeIndex]); + } + + private void installTargetChangeListener() { + mSdkTargetChangeListener = new TargetChangeListener() { + @Override + public IProject getProject() { + return mValues.project; + } + + @Override + public void reload() { + if (mValues.project != null) { + changeProject(mValues.project); + } + } + }; + + AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); + } + + @Override + public void dispose() { + + if (mSdkTargetChangeListener != null) { + AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); + mSdkTargetChangeListener = null; + } + + super.dispose(); + } + + /** + * Returns the selected root element string, if any. + * + * @return The selected root element string or null. + */ + public String getRootElement() { + int index = mRootTable.getSelectionIndex(); + if (index >= 0) { + Object[] roots = (Object[]) mRootTableViewer.getInput(); + return roots[index].toString(); + } + return null; + } + + /** + * Called by {@link NewXmlFileWizard} to initialize the page with the selection + * received by the wizard -- typically the current user workbench selection. + * <p/> + * Things we expect to find out from the selection: + * <ul> + * <li>The project name, valid if it's an android nature.</li> + * <li>The current folder, valid if it's a folder under /res</li> + * <li>An existing filename, in which case the user will be asked whether to override it.</li> + * </ul> + * <p/> + * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace + * resource path (where the resource path does not have to exist yet, such as res/anim/). + * + * @param selection The selection when the wizard was initiated. + */ + private boolean initializeFromSelection(IStructuredSelection selection) { + if (selection == null) { + return false; + } + + // Find the best match in the element list. In case there are multiple selected elements + // select the one that provides the most information and assign them a score, + // e.g. project=1 + folder=2 + file=4. + IProject targetProject = null; + String targetWsFolderPath = null; + String targetFileName = null; + int targetScore = 0; + for (Object element : selection.toList()) { + if (element instanceof IAdaptable) { + IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); + IProject project = res != null ? res.getProject() : null; + + // Is this an Android project? + try { + if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) { + continue; + } + } catch (CoreException e) { + // checking the nature failed, ignore this resource + continue; + } + + int score = 1; // we have a valid project at least + + IPath wsFolderPath = null; + String fileName = null; + assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no + if (res.getType() == IResource.FOLDER) { + wsFolderPath = res.getProjectRelativePath(); + } else if (res.getType() == IResource.FILE) { + if (SdkUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) { + fileName = res.getName(); + } + wsFolderPath = res.getParent().getProjectRelativePath(); + } + + // Disregard this folder selection if it doesn't point to /res/something + if (wsFolderPath != null && + wsFolderPath.segmentCount() > 1 && + SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { + score += 2; + } else { + wsFolderPath = null; + fileName = null; + } + + score += fileName != null ? 4 : 0; + + if (score > targetScore) { + targetScore = score; + targetProject = project; + targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; + targetFileName = fileName; + } + } else if (element instanceof Pair<?,?>) { + // Pair of Project/String + @SuppressWarnings("unchecked") + Pair<IProject,String> pair = (Pair<IProject,String>)element; + targetScore = 1; + targetProject = pair.getFirst(); + targetWsFolderPath = pair.getSecond(); + targetFileName = ""; + } + } + + if (targetProject == null) { + // Try to figure out the project from the active editor + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + IEditorPart activeEditor = page.getActiveEditor(); + if (activeEditor instanceof AndroidXmlEditor) { + Object input = ((AndroidXmlEditor) activeEditor).getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + targetScore = 1; + IFile file = fileInput.getFile(); + targetProject = file.getProject(); + IPath path = file.getParent().getProjectRelativePath(); + targetWsFolderPath = path != null ? path.toString() : null; + } + } + } + } + } + + if (targetProject == null) { + // If we didn't find a default project based on the selection, check how many + // open Android projects we can find in the current workspace. If there's only + // one, we'll just select it by default. + IJavaProject[] projects = AdtUtils.getOpenAndroidProjects(); + if (projects != null && projects.length == 1) { + targetScore = 1; + targetProject = projects[0].getProject(); + } + } + + // Now set the UI accordingly + if (targetScore > 0) { + mValues.project = targetProject; + mValues.folderPath = targetWsFolderPath; + mProjectButton.setSelectedProject(targetProject); + mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ + + // If the current selection context corresponds to a specific file type, + // select it. + if (targetWsFolderPath != null) { + int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR); + if (pos >= 0) { + targetWsFolderPath = targetWsFolderPath.substring(pos + 1); + } + String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP); + if (folderSegments.length > 0) { + mValues.configuration = FolderConfiguration.getConfig(folderSegments); + String folderName = folderSegments[0]; + selectTypeFromFolder(folderName); + } + } + } + + return true; + } + + private void initializeFromFixedType() { + if (mInitialFolderType != null) { + for (TypeInfo type : sTypes) { + if (type.getResFolderType() == mInitialFolderType) { + mValues.type = type; + updateFolderPath(type); + break; + } + } + } + } + + /** + * Given a folder name, such as "drawable", select the corresponding type in + * the dropdown. + */ + void selectTypeFromFolder(String folderName) { + List<TypeInfo> matches = new ArrayList<TypeInfo>(); + boolean selected = false; + + TypeInfo selectedType = getSelectedType(); + for (TypeInfo type : sTypes) { + if (type.getResFolderName().equals(folderName)) { + matches.add(type); + selected |= type == selectedType; + } + } + + if (matches.size() == 1) { + // If there's only one match, select it if it's not already selected + if (!selected) { + selectType(matches.get(0)); + } + } else if (matches.size() > 1) { + // There are multiple type candidates for this folder. This can happen + // for /res/xml for example. Check to see if one of them is currently + // selected. If yes, leave the selection unchanged. If not, deselect all type. + if (!selected) { + selectType(null); + } + } else { + // Nothing valid was selected. + selectType(null); + } + } + + /** + * Initialize the root values of the type infos based on the current framework values. + */ + private void initializeRootValues() { + IProject project = mValues.project; + for (TypeInfo type : sTypes) { + // Clear all the roots for this type + ArrayList<String> roots = type.getRoots(); + if (roots.size() > 0) { + roots.clear(); + } + + // depending of the type of the seed, initialize the root in different ways + Object rootSeed = type.getRootSeed(); + + if (rootSeed instanceof String) { + // The seed is a single string, Add it as-is. + roots.add((String) rootSeed); + } else if (rootSeed instanceof String[]) { + // The seed is an array of strings. Add them as-is. + for (String value : (String[]) rootSeed) { + roots.add(value); + } + } else if (rootSeed instanceof Integer && project != null) { + // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_* + // In this case add all the children element descriptors defined, recursively, + // and avoid infinite recursion by keeping track of what has already been added. + + // Note: if project is null, the root list will be empty since it has been + // cleared above. + + // get the AndroidTargetData from the project + IAndroidTarget target = null; + AndroidTargetData data = null; + + target = Sdk.getCurrent().getTarget(project); + if (target == null) { + // A project should have a target. The target can be missing if the project + // is an old project for which a target hasn't been affected or if the + // target no longer exists in this SDK. Simply log the error and dismiss. + + AdtPlugin.log(IStatus.INFO, + "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$ + project.getName()); + continue; + } else { + data = Sdk.getCurrent().getTargetData(target); + + if (data == null) { + // We should have both a target and its data. + // However if the wizard is invoked whilst the platform is still being + // loaded we can end up in a weird case where we have a target but it + // doesn't have any data yet. + // Lets log a warning and silently ignore this root. + + AdtPlugin.log(IStatus.INFO, + "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$ + target.getName(), project.getName()); + continue; + } + } + + IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); + ElementDescriptor descriptor = provider.getDescriptor(); + if (descriptor != null) { + HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>(); + initRootElementDescriptor(roots, descriptor, visited); + } + + // Sort alphabetically. + Collections.sort(roots); + } + } + } + + /** + * Helper method to recursively insert all XML names for the given {@link ElementDescriptor} + * into the roots array list. Keeps track of visited nodes to avoid infinite recursion. + * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic + * and not a valid root element. + */ + private void initRootElementDescriptor(ArrayList<String> roots, + ElementDescriptor desc, HashSet<ElementDescriptor> visited) { + if (!(desc instanceof DocumentDescriptor)) { + String xmlName = desc.getXmlName(); + if (xmlName != null && xmlName.length() > 0) { + roots.add(xmlName); + } + } + + visited.add(desc); + + for (ElementDescriptor child : desc.getChildren()) { + if (!visited.contains(child)) { + initRootElementDescriptor(roots, child, visited); + } + } + } + + /** + * Changes mProject to the given new project and update the UI accordingly. + * <p/> + * Note that this does not check if the new project is the same as the current one + * on purpose, which allows a project to be updated when its target has changed or + * when targets are loaded in the background. + */ + private void changeProject(IProject newProject) { + mValues.project = newProject; + + // enable types based on new API level + updateAvailableTypes(); + initialSelectType(); + + // update the folder name based on API level + updateFolderPath(mValues.type); + + // update the Type with the new descriptors. + initializeRootValues(); + + // update the combo + updateRootCombo(mValues.type); + + validatePage(); + } + + private void onSelectType(TypeInfo type) { + // Do nothing if this is an internal modification or if the widget has been + // deselected. + if (mInternalTypeUpdate) { + return; + } + + mValues.type = type; + + if (type == null) { + return; + } + + // update the combo + updateRootCombo(type); + + // update the folder path + updateFolderPath(type); + + validatePage(); + } + + /** Updates the selected type in the type dropdown control */ + private void setSelectedType(TypeInfo type) { + TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); + if (types != null) { + for (int i = 0, n = types.length; i < n; i++) { + if (types[i] == type) { + mTypeCombo.select(i); + break; + } + } + } + } + + /** Returns the selected type in the type dropdown control */ + private TypeInfo getSelectedType() { + int index = mTypeCombo.getSelectionIndex(); + if (index != -1) { + TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); + return types[index]; + } + + return null; + } + + /** Returns the selected index in the type dropdown control */ + private int getTypeComboIndex(TypeInfo type) { + TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); + for (int i = 0, n = types.length; i < n; i++) { + if (type == types[i]) { + return i; + } + } + + return -1; + } + + /** Updates the folder path to reflect the given type */ + private void updateFolderPath(TypeInfo type) { + String wsFolderPath = mValues.folderPath; + String newPath = null; + FolderConfiguration config = mValues.configuration; + ResourceQualifier qual = config.getInvalidQualifier(); + if (qual == null) { + // The configuration is valid. Reformat the folder path using the canonical + // value from the configuration. + newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); + } else { + // The configuration is invalid. We still update the path but this time + // do it manually on the string. + if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { + wsFolderPath = wsFolderPath.replaceFirst( + "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$ + "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); + } + } + + if (newPath != null && !newPath.equals(wsFolderPath)) { + mValues.folderPath = newPath; + } + } + + /** + * Helper method that fills the values of the "root element" combo box based + * on the currently selected type radio button. Also disables the combo is there's + * only one choice. Always select the first root element for the given type. + * + * @param type The currently selected {@link TypeInfo}, or null + */ + private void updateRootCombo(TypeInfo type) { + IBaseLabelProvider labelProvider = new ColumnLabelProvider() { + @Override + public Image getImage(Object element) { + return IconFactory.getInstance().getIcon(element.toString()); + } + }; + mRootTableViewer.setContentProvider(new ArrayContentProvider()); + mRootTableViewer.setLabelProvider(labelProvider); + + if (type != null) { + // get the list of roots. The list can be empty but not null. + ArrayList<String> roots = type.getRoots(); + mRootTableViewer.setInput(roots.toArray()); + + int index = 0; // default is to select the first one + String defaultRoot = type.getDefaultRoot(mValues.project); + if (defaultRoot != null) { + index = roots.indexOf(defaultRoot); + } + mRootTable.select(index < 0 ? 0 : index); + mRootTable.showSelection(); + } + } + + /** + * Helper method to select the current type in the type dropdown + * + * @param type The TypeInfo matching the radio button to selected or null to deselect them all. + */ + private void selectType(TypeInfo type) { + mInternalTypeUpdate = true; + mValues.type = type; + if (type == null) { + if (mTypeCombo.getSelectionIndex() != -1) { + mTypeCombo.deselect(mTypeCombo.getSelectionIndex()); + } + } else { + setSelectedType(type); + } + updateRootCombo(type); + mInternalTypeUpdate = false; + } + + /** + * Add the available types in the type combobox, based on whether they are available + * for the current SDK. + * <p/> + * A type is available either if: + * - if mProject is null, API level 1 is considered valid + * - if mProject is !null, the project->target->API must be >= to the type's API level. + */ + private void updateAvailableTypes() { + IProject project = mValues.project; + IAndroidTarget target = project != null ? Sdk.getCurrent().getTarget(project) : null; + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getVersion().getApiLevel(); + } + + List<String> items = new ArrayList<String>(sTypes.length); + List<TypeInfo> types = new ArrayList<TypeInfo>(sTypes.length); + for (int i = 0, n = sTypes.length; i < n; i++) { + TypeInfo type = sTypes[i]; + if (type.getTargetApiLevel() <= currentApiLevel) { + items.add(type.getUiName()); + types.add(type); + } + } + mTypeCombo.setItems(items.toArray(new String[items.size()])); + mTypeCombo.setData(types.toArray(new TypeInfo[types.size()])); + } + + /** + * Validates the fields, displays errors and warnings. + * Enables the finish button if there are no errors. + */ + private void validatePage() { + String error = null; + String warning = null; + + // -- validate type + TypeInfo type = mValues.type; + if (error == null) { + if (type == null) { + error = "One of the types must be selected (e.g. layout, values, etc.)"; + } + } + + // -- validate project + if (mValues.project == null) { + error = "Please select an Android project."; + } + + // -- validate type API level + if (error == null) { + IAndroidTarget target = Sdk.getCurrent().getTarget(mValues.project); + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getVersion().getApiLevel(); + } + + assert type != null; + if (type.getTargetApiLevel() > currentApiLevel) { + error = "The API level of the selected type (e.g. AppWidget, etc.) is not " + + "compatible with the API level of the project."; + } + } + + // -- validate filename + if (error == null) { + String fileName = mValues.getFileName(); + assert type != null; + ResourceFolderType folderType = type.getResFolderType(); + error = ResourceNameValidator.create(true, folderType).isValid(fileName); + } + + // -- validate destination file doesn't exist + if (error == null) { + IFile file = mValues.getDestinationFile(); + if (file != null && file.exists()) { + warning = "The destination file already exists"; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, IMessageProvider.ERROR); + } else if (warning != null) { + setMessage(warning, IMessageProvider.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + + /** + * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if + * not found + * + * @param folderType the {@link ResourceFolderType} to look for + * @return the corresponding {@link TypeInfo} + */ + static TypeInfo getTypeInfo(ResourceFolderType folderType) { + for (TypeInfo typeInfo : sTypes) { + if (typeInfo.getResFolderType() == folderType) { + return typeInfo; + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java new file mode 100644 index 000000000..16cd7b355 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2008 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.newxmlfile; + +import static com.android.SdkConstants.FQCN_GRID_LAYOUT; +import static com.android.SdkConstants.GRID_LAYOUT; + +import com.android.SdkConstants; +import com.android.ide.common.resources.configuration.FolderConfiguration; +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.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +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.RenderPreviewManager; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo; +import com.android.resources.ResourceFolderType; +import com.android.utils.Pair; + +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.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PartInitException; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * The "New Android XML File Wizard" provides the ability to create skeleton XML + * resources files for Android projects. + * <p/> + * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project, + * the resource folder, resource type and file name. It then creates the XML file. + */ +public class NewXmlFileWizard extends Wizard implements INewWizard { + /** The XML header to write at the top of the XML file */ + public static final String XML_HEADER_LINE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$ + + private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$ + + protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$ + + private NewXmlFileCreationPage mMainPage; + private ChooseConfigurationPage mConfigPage; + private Values mValues; + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + setHelpAvailable(false); // TODO have help + setWindowTitle("New Android XML File"); + setImageDescriptor(); + + mValues = new Values(); + mMainPage = createMainPage(mValues); + mMainPage.setTitle("New Android XML File"); + mMainPage.setDescription("Creates a new Android XML file."); + mMainPage.setInitialSelection(selection); + + mConfigPage = new ChooseConfigurationPage(mValues); + + // Trigger a check to see if the SDK needs to be reloaded (which will + // invoke onSdkLoaded asynchronously as needed). + AdtPlugin.getDefault().refreshSdk(); + } + + /** + * Creates the wizard page. + * <p/> + * Please do NOT override this method. + * <p/> + * This is protected so that it can be overridden by unit tests. + * However the contract of this class is private and NO ATTEMPT will be made + * to maintain compatibility between different versions of the plugin. + */ + protected NewXmlFileCreationPage createMainPage(NewXmlFileWizard.Values values) { + return new NewXmlFileCreationPage(MAIN_PAGE_NAME, values); + } + + // -- Methods inherited from org.eclipse.jface.wizard.Wizard -- + // + // The Wizard class implements most defaults and boilerplate code needed by + // IWizard + + /** + * Adds pages to this wizard. + */ + @Override + public void addPages() { + addPage(mMainPage); + addPage(mConfigPage); + + } + + /** + * Performs any actions appropriate in response to the user having pressed + * the Finish button, or refuse if finishing now is not permitted: here, it + * actually creates the workspace project and then switch to the Java + * perspective. + * + * @return True + */ + @Override + public boolean performFinish() { + final Pair<IFile, IRegion> created = createXmlFile(); + if (created == null) { + return false; + } else { + // Open the file + // This has to be delayed in order for focus handling to work correctly + AdtPlugin.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + IFile file = created.getFirst(); + IRegion region = created.getSecond(); + try { + IEditorPart editor = AdtPlugin.openFile(file, null, + false /*showEditorTab*/); + if (editor instanceof AndroidXmlEditor) { + final AndroidXmlEditor xmlEditor = (AndroidXmlEditor)editor; + if (!xmlEditor.hasMultiplePages()) { + xmlEditor.show(region.getOffset(), region.getLength(), + true /* showEditorTab */); + } + } + } catch (PartInitException e) { + AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$ + file.getFullPath().toString()); + } + }}); + + return true; + } + } + + // -- Custom Methods -- + + private Pair<IFile, IRegion> createXmlFile() { + IFile file = mValues.getDestinationFile(); + TypeInfo type = mValues.type; + if (type == null) { + // this is not expected to happen + String name = file.getFullPath().toString(); + AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name); //$NON-NLS-1$ + return null; + } + String xmlns = type.getXmlns(); + String root = mMainPage.getRootElement(); + if (root == null) { + // this is not expected to happen + AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$ + file.toString()); + return null; + } + + String attrs = type.getDefaultAttrs(mValues.project, root); + String child = type.getChild(mValues.project, root); + return createXmlFile(file, xmlns, root, attrs, child, type.getResFolderType()); + } + + /** Creates a new file using the given root element, namespace and root attributes */ + private static Pair<IFile, IRegion> createXmlFile(IFile file, String xmlns, + String root, String rootAttributes, String child, ResourceFolderType folderType) { + String name = file.getFullPath().toString(); + boolean need_delete = false; + + if (file.exists()) { + if (!AdtPlugin.displayPrompt("New Android XML File", + String.format("Do you want to overwrite the file %1$s ?", name))) { + // abort if user selects cancel. + return null; + } + need_delete = true; + } else { + AdtUtils.createWsParentDirectory(file.getParent()); + } + + StringBuilder sb = new StringBuilder(XML_HEADER_LINE); + + if (folderType == ResourceFolderType.LAYOUT && root.equals(GRID_LAYOUT)) { + IProject project = file.getParent().getProject(); + int minSdk = ManifestInfo.get(project).getMinSdkVersion(); + if (minSdk < 14) { + root = SupportLibraryHelper.getTagFor(project, FQCN_GRID_LAYOUT); + if (root.equals(FQCN_GRID_LAYOUT)) { + root = GRID_LAYOUT; + } + } + } + + sb.append('<').append(root); + if (xmlns != null) { + sb.append('\n').append(" xmlns:android=\"").append(xmlns).append('"'); //$NON-NLS-1$ + } + + if (rootAttributes != null) { + sb.append("\n "); //$NON-NLS-1$ + sb.append(rootAttributes.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$ + } + + sb.append(">\n"); //$NON-NLS-1$ + + if (child != null) { + sb.append(child); + } + + boolean autoFormat = AdtPrefs.getPrefs().getUseCustomXmlFormatter(); + + // Insert an indented caret. Since the markup here will be reformatted, we need to + // insert text tokens that the formatter will preserve, which we can then turn back + // into indentation and a caret offset: + final String indentToken = "${indent}"; //$NON-NLS-1$ + final String caretToken = "${caret}"; //$NON-NLS-1$ + sb.append(indentToken); + sb.append(caretToken); + if (!autoFormat) { + sb.append('\n'); + } + + sb.append("</").append(root).append(">\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); + String fileContents; + if (!autoFormat) { + fileContents = sb.toString(); + } else { + XmlFormatStyle style = EclipseXmlPrettyPrinter.getForFolderType(folderType); + fileContents = EclipseXmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs, + style, null /*lineSeparator*/); + } + + // Remove marker tokens and replace them with whitespace + fileContents = fileContents.replace(indentToken, formatPrefs.getOneIndentUnit()); + int caretOffset = fileContents.indexOf(caretToken); + if (caretOffset != -1) { + fileContents = fileContents.replace(caretToken, ""); //$NON-NLS-1$ + } + + String error = null; + try { + byte[] buf = fileContents.getBytes("UTF8"); //$NON-NLS-1$ + InputStream stream = new ByteArrayInputStream(buf); + if (need_delete) { + file.delete(IResource.KEEP_HISTORY | IResource.FORCE, null /*monitor*/); + } + file.create(stream, true /*force*/, null /*progress*/); + IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null; + + // If you introduced a new locale, or new screen variations etc, ensure that + // the list of render previews is updated if necessary + if (file.getParent().getName().indexOf('-') != -1 + && (folderType == ResourceFolderType.LAYOUT + || folderType == ResourceFolderType.VALUES)) { + RenderPreviewManager.bumpRevision(); + } + + return Pair.of(file, region); + } catch (UnsupportedEncodingException e) { + error = e.getMessage(); + } catch (CoreException e) { + error = e.getMessage(); + } + + error = String.format("Failed to generate %1$s: %2$s", name, error); + AdtPlugin.displayError("New Android XML File", error); + return null; + } + + /** + * Returns true if the New XML Wizard can create new files of the given + * {@link ResourceFolderType} + * + * @param folderType the folder type to create a file for + * @return true if this wizard can create new files for the given folder type + */ + public static boolean canCreateXmlFile(ResourceFolderType folderType) { + TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType); + return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null || + typeInfo.getRootSeed() instanceof String); + } + + /** + * Creates a new XML file using the template according to the given folder type + * + * @param project the project to create the file in + * @param file the file to be created + * @param folderType the type of folder to look up a template for + * @return the created file + */ + public static Pair<IFile, IRegion> createXmlFile(IProject project, IFile file, + ResourceFolderType folderType) { + TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType); + String xmlns = type.getXmlns(); + String root = type.getDefaultRoot(project); + if (root == null) { + root = type.getRootSeed().toString(); + } + String attrs = type.getDefaultAttrs(project, root); + return createXmlFile(file, xmlns, root, attrs, null, folderType); + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + + /** + * Specific New XML File wizard tied to the {@link ResourceFolderType#LAYOUT} type + */ + public static class NewLayoutWizard extends NewXmlFileWizard { + /** Creates a new {@link NewLayoutWizard} */ + public NewLayoutWizard() { + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + setWindowTitle("New Android Layout XML File"); + super.mMainPage.setTitle("New Android Layout XML File"); + super.mMainPage.setDescription("Creates a new Android Layout XML file."); + super.mMainPage.setInitialFolderType(ResourceFolderType.LAYOUT); + } + } + + /** + * Specific New XML File wizard tied to the {@link ResourceFolderType#VALUES} type + */ + public static class NewValuesWizard extends NewXmlFileWizard { + /** Creates a new {@link NewValuesWizard} */ + public NewValuesWizard() { + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + super.init(workbench, selection); + setWindowTitle("New Android Values XML File"); + super.mMainPage.setTitle("New Android Values XML File"); + super.mMainPage.setDescription("Creates a new Android Values XML file."); + super.mMainPage.setInitialFolderType(ResourceFolderType.VALUES); + } + } + + /** Value object which holds the current state of the wizard pages */ + public static class Values { + /** The currently selected project, or null */ + public IProject project; + /** The root name of the XML file to create, or null */ + public String name; + /** The type of XML file to create */ + public TypeInfo type; + /** The path within the project to create the new file in */ + public String folderPath; + /** The currently chosen configuration */ + public FolderConfiguration configuration = new FolderConfiguration(); + + /** + * Returns the destination filename or an empty string. + * + * @return the filename, never null. + */ + public String getFileName() { + String fileName; + if (name == null) { + fileName = ""; //$NON-NLS-1$ + } else { + fileName = name.trim(); + if (fileName.length() > 0 && fileName.indexOf('.') == -1) { + fileName = fileName + SdkConstants.DOT_XML; + } + } + + return fileName; + } + + /** + * Returns a {@link IFile} for the destination file. + * <p/> + * Returns null if the project, filename or folder are invalid and the + * destination file cannot be determined. + * <p/> + * The {@link IFile} is a resource. There might or might not be an + * actual real file. + * + * @return an {@link IFile} for the destination file + */ + public IFile getDestinationFile() { + String fileName = getFileName(); + if (project != null && folderPath != null && folderPath.length() > 0 + && fileName.length() > 0) { + IPath dest = new Path(folderPath).append(fileName); + IFile file = project.getFile(dest); + return file; + } + return null; + } + } +} 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) { + } +} |