Home | History | Annotate | Download | only in newxmlfile
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
     18 
     19 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
     20 import static com.android.SdkConstants.GRID_LAYOUT;
     21 
     22 import com.android.SdkConstants;
     23 import com.android.ide.common.resources.configuration.FolderConfiguration;
     24 import com.android.ide.common.xml.XmlFormatStyle;
     25 import com.android.ide.eclipse.adt.AdtPlugin;
     26 import com.android.ide.eclipse.adt.AdtUtils;
     27 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     28 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     29 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
     30 import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
     31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager;
     32 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     33 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     34 import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper;
     35 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
     36 import com.android.resources.ResourceFolderType;
     37 import com.android.utils.Pair;
     38 
     39 import org.eclipse.core.resources.IFile;
     40 import org.eclipse.core.resources.IProject;
     41 import org.eclipse.core.resources.IResource;
     42 import org.eclipse.core.runtime.CoreException;
     43 import org.eclipse.core.runtime.IPath;
     44 import org.eclipse.core.runtime.IStatus;
     45 import org.eclipse.core.runtime.Path;
     46 import org.eclipse.jface.resource.ImageDescriptor;
     47 import org.eclipse.jface.text.IRegion;
     48 import org.eclipse.jface.text.Region;
     49 import org.eclipse.jface.viewers.IStructuredSelection;
     50 import org.eclipse.jface.wizard.Wizard;
     51 import org.eclipse.ui.IEditorPart;
     52 import org.eclipse.ui.INewWizard;
     53 import org.eclipse.ui.IWorkbench;
     54 import org.eclipse.ui.PartInitException;
     55 
     56 import java.io.ByteArrayInputStream;
     57 import java.io.InputStream;
     58 import java.io.UnsupportedEncodingException;
     59 
     60 /**
     61  * The "New Android XML File Wizard" provides the ability to create skeleton XML
     62  * resources files for Android projects.
     63  * <p/>
     64  * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project,
     65  * the resource folder, resource type and file name. It then creates the XML file.
     66  */
     67 public class NewXmlFileWizard extends Wizard implements INewWizard {
     68     /** The XML header to write at the top of the XML file */
     69     public static final String XML_HEADER_LINE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$
     70 
     71     private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$
     72 
     73     protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$
     74 
     75     private NewXmlFileCreationPage mMainPage;
     76     private ChooseConfigurationPage mConfigPage;
     77     private Values mValues;
     78 
     79     @Override
     80     public void init(IWorkbench workbench, IStructuredSelection selection) {
     81         setHelpAvailable(false); // TODO have help
     82         setWindowTitle("New Android XML File");
     83         setImageDescriptor();
     84 
     85         mValues = new Values();
     86         mMainPage = createMainPage(mValues);
     87         mMainPage.setTitle("New Android XML File");
     88         mMainPage.setDescription("Creates a new Android XML file.");
     89         mMainPage.setInitialSelection(selection);
     90 
     91         mConfigPage = new ChooseConfigurationPage(mValues);
     92 
     93         // Trigger a check to see if the SDK needs to be reloaded (which will
     94         // invoke onSdkLoaded asynchronously as needed).
     95         AdtPlugin.getDefault().refreshSdk();
     96     }
     97 
     98     /**
     99      * Creates the wizard page.
    100      * <p/>
    101      * Please do NOT override this method.
    102      * <p/>
    103      * This is protected so that it can be overridden by unit tests.
    104      * However the contract of this class is private and NO ATTEMPT will be made
    105      * to maintain compatibility between different versions of the plugin.
    106      */
    107     protected NewXmlFileCreationPage createMainPage(NewXmlFileWizard.Values values) {
    108         return new NewXmlFileCreationPage(MAIN_PAGE_NAME, values);
    109     }
    110 
    111     // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
    112     //
    113     // The Wizard class implements most defaults and boilerplate code needed by
    114     // IWizard
    115 
    116     /**
    117      * Adds pages to this wizard.
    118      */
    119     @Override
    120     public void addPages() {
    121         addPage(mMainPage);
    122         addPage(mConfigPage);
    123 
    124     }
    125 
    126     /**
    127      * Performs any actions appropriate in response to the user having pressed
    128      * the Finish button, or refuse if finishing now is not permitted: here, it
    129      * actually creates the workspace project and then switch to the Java
    130      * perspective.
    131      *
    132      * @return True
    133      */
    134     @Override
    135     public boolean performFinish() {
    136         final Pair<IFile, IRegion> created = createXmlFile();
    137         if (created == null) {
    138             return false;
    139         } else {
    140             // Open the file
    141             // This has to be delayed in order for focus handling to work correctly
    142             AdtPlugin.getDisplay().asyncExec(new Runnable() {
    143                 @Override
    144                 public void run() {
    145                     IFile file = created.getFirst();
    146                     IRegion region = created.getSecond();
    147                     try {
    148                         IEditorPart editor = AdtPlugin.openFile(file, null,
    149                                 false /*showEditorTab*/);
    150                         if (editor instanceof AndroidXmlEditor) {
    151                             final AndroidXmlEditor xmlEditor = (AndroidXmlEditor)editor;
    152                             if (!xmlEditor.hasMultiplePages()) {
    153                                 xmlEditor.show(region.getOffset(), region.getLength(),
    154                                         true /* showEditorTab */);
    155                             }
    156                         }
    157                     } catch (PartInitException e) {
    158                         AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$
    159                                 file.getFullPath().toString());
    160                     }
    161                 }});
    162 
    163             return true;
    164         }
    165     }
    166 
    167     // -- Custom Methods --
    168 
    169     private Pair<IFile, IRegion> createXmlFile() {
    170         IFile file = mValues.getDestinationFile();
    171         TypeInfo type = mValues.type;
    172         if (type == null) {
    173             // this is not expected to happen
    174             String name = file.getFullPath().toString();
    175             AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name);  //$NON-NLS-1$
    176             return null;
    177         }
    178         String xmlns = type.getXmlns();
    179         String root = mMainPage.getRootElement();
    180         if (root == null) {
    181             // this is not expected to happen
    182             AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$
    183                     file.toString());
    184             return null;
    185         }
    186 
    187         String attrs = type.getDefaultAttrs(mValues.project, root);
    188         String child = type.getChild(mValues.project, root);
    189         return createXmlFile(file, xmlns, root, attrs, child, type.getResFolderType());
    190     }
    191 
    192     /** Creates a new file using the given root element, namespace and root attributes */
    193     private static Pair<IFile, IRegion> createXmlFile(IFile file, String xmlns,
    194             String root, String rootAttributes, String child, ResourceFolderType folderType) {
    195         String name = file.getFullPath().toString();
    196         boolean need_delete = false;
    197 
    198         if (file.exists()) {
    199             if (!AdtPlugin.displayPrompt("New Android XML File",
    200                 String.format("Do you want to overwrite the file %1$s ?", name))) {
    201                 // abort if user selects cancel.
    202                 return null;
    203             }
    204             need_delete = true;
    205         } else {
    206             AdtUtils.createWsParentDirectory(file.getParent());
    207         }
    208 
    209         StringBuilder sb = new StringBuilder(XML_HEADER_LINE);
    210 
    211         if (folderType == ResourceFolderType.LAYOUT && root.equals(GRID_LAYOUT)) {
    212             IProject project = file.getParent().getProject();
    213             int minSdk = ManifestInfo.get(project).getMinSdkVersion();
    214             if (minSdk < 14) {
    215                 root = SupportLibraryHelper.getTagFor(project, FQCN_GRID_LAYOUT);
    216                 if (root.equals(FQCN_GRID_LAYOUT)) {
    217                     root = GRID_LAYOUT;
    218                 }
    219             }
    220         }
    221 
    222         sb.append('<').append(root);
    223         if (xmlns != null) {
    224             sb.append('\n').append("  xmlns:android=\"").append(xmlns).append('"');  //$NON-NLS-1$
    225         }
    226 
    227         if (rootAttributes != null) {
    228             sb.append("\n  ");                       //$NON-NLS-1$
    229             sb.append(rootAttributes.replace("\n", "\n  "));  //$NON-NLS-1$ //$NON-NLS-2$
    230         }
    231 
    232         sb.append(">\n");                            //$NON-NLS-1$
    233 
    234         if (child != null) {
    235             sb.append(child);
    236         }
    237 
    238         boolean autoFormat = AdtPrefs.getPrefs().getUseCustomXmlFormatter();
    239 
    240         // Insert an indented caret. Since the markup here will be reformatted, we need to
    241         // insert text tokens that the formatter will preserve, which we can then turn back
    242         // into indentation and a caret offset:
    243         final String indentToken = "${indent}"; //$NON-NLS-1$
    244         final String caretToken = "${caret}";   //$NON-NLS-1$
    245         sb.append(indentToken);
    246         sb.append(caretToken);
    247         if (!autoFormat) {
    248             sb.append('\n');
    249         }
    250 
    251         sb.append("</").append(root).append(">\n");  //$NON-NLS-1$ //$NON-NLS-2$
    252 
    253         EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create();
    254         String fileContents;
    255         if (!autoFormat) {
    256             fileContents = sb.toString();
    257         } else {
    258             XmlFormatStyle style = EclipseXmlPrettyPrinter.getForFolderType(folderType);
    259             fileContents = EclipseXmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs,
    260                     style, null /*lineSeparator*/);
    261         }
    262 
    263         // Remove marker tokens and replace them with whitespace
    264         fileContents = fileContents.replace(indentToken, formatPrefs.getOneIndentUnit());
    265         int caretOffset = fileContents.indexOf(caretToken);
    266         if (caretOffset != -1) {
    267             fileContents = fileContents.replace(caretToken, ""); //$NON-NLS-1$
    268         }
    269 
    270         String error = null;
    271         try {
    272             byte[] buf = fileContents.getBytes("UTF8");    //$NON-NLS-1$
    273             InputStream stream = new ByteArrayInputStream(buf);
    274             if (need_delete) {
    275                 file.delete(IResource.KEEP_HISTORY | IResource.FORCE, null /*monitor*/);
    276             }
    277             file.create(stream, true /*force*/, null /*progress*/);
    278             IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null;
    279 
    280             // If you introduced a new locale, or new screen variations etc, ensure that
    281             // the list of render previews is updated if necessary
    282             if (file.getParent().getName().indexOf('-') != -1
    283                     && (folderType == ResourceFolderType.LAYOUT
    284                         || folderType == ResourceFolderType.VALUES)) {
    285                 RenderPreviewManager.bumpRevision();
    286             }
    287 
    288             return Pair.of(file, region);
    289         } catch (UnsupportedEncodingException e) {
    290             error = e.getMessage();
    291         } catch (CoreException e) {
    292             error = e.getMessage();
    293         }
    294 
    295         error = String.format("Failed to generate %1$s: %2$s", name, error);
    296         AdtPlugin.displayError("New Android XML File", error);
    297         return null;
    298     }
    299 
    300     /**
    301      * Returns true if the New XML Wizard can create new files of the given
    302      * {@link ResourceFolderType}
    303      *
    304      * @param folderType the folder type to create a file for
    305      * @return true if this wizard can create new files for the given folder type
    306      */
    307     public static boolean canCreateXmlFile(ResourceFolderType folderType) {
    308         TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType);
    309         return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null ||
    310                 typeInfo.getRootSeed() instanceof String);
    311     }
    312 
    313     /**
    314      * Creates a new XML file using the template according to the given folder type
    315      *
    316      * @param project the project to create the file in
    317      * @param file the file to be created
    318      * @param folderType the type of folder to look up a template for
    319      * @return the created file
    320      */
    321     public static Pair<IFile, IRegion> createXmlFile(IProject project, IFile file,
    322             ResourceFolderType folderType) {
    323         TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType);
    324         String xmlns = type.getXmlns();
    325         String root = type.getDefaultRoot(project);
    326         if (root == null) {
    327             root = type.getRootSeed().toString();
    328         }
    329         String attrs = type.getDefaultAttrs(project, root);
    330         return createXmlFile(file, xmlns, root, attrs, null, folderType);
    331     }
    332 
    333     /**
    334      * Returns an image descriptor for the wizard logo.
    335      */
    336     private void setImageDescriptor() {
    337         ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
    338         setDefaultPageImageDescriptor(desc);
    339     }
    340 
    341     /**
    342      * Specific New XML File wizard tied to the {@link ResourceFolderType#LAYOUT} type
    343      */
    344     public static class NewLayoutWizard extends NewXmlFileWizard {
    345         /** Creates a new {@link NewLayoutWizard} */
    346         public NewLayoutWizard() {
    347         }
    348 
    349         @Override
    350         public void init(IWorkbench workbench, IStructuredSelection selection) {
    351             super.init(workbench, selection);
    352             setWindowTitle("New Android Layout XML File");
    353             super.mMainPage.setTitle("New Android Layout XML File");
    354             super.mMainPage.setDescription("Creates a new Android Layout XML file.");
    355             super.mMainPage.setInitialFolderType(ResourceFolderType.LAYOUT);
    356         }
    357     }
    358 
    359     /**
    360      * Specific New XML File wizard tied to the {@link ResourceFolderType#VALUES} type
    361      */
    362     public static class NewValuesWizard extends NewXmlFileWizard {
    363         /** Creates a new {@link NewValuesWizard} */
    364         public NewValuesWizard() {
    365         }
    366 
    367         @Override
    368         public void init(IWorkbench workbench, IStructuredSelection selection) {
    369             super.init(workbench, selection);
    370             setWindowTitle("New Android Values XML File");
    371             super.mMainPage.setTitle("New Android Values XML File");
    372             super.mMainPage.setDescription("Creates a new Android Values XML file.");
    373             super.mMainPage.setInitialFolderType(ResourceFolderType.VALUES);
    374         }
    375     }
    376 
    377     /** Value object which holds the current state of the wizard pages */
    378     public static class Values {
    379         /** The currently selected project, or null */
    380         public IProject project;
    381         /** The root name of the XML file to create, or null */
    382         public String name;
    383         /** The type of XML file to create */
    384         public TypeInfo type;
    385         /** The path within the project to create the new file in */
    386         public String folderPath;
    387         /** The currently chosen configuration */
    388         public FolderConfiguration configuration = new FolderConfiguration();
    389 
    390         /**
    391          * Returns the destination filename or an empty string.
    392          *
    393          * @return the filename, never null.
    394          */
    395         public String getFileName() {
    396             String fileName;
    397             if (name == null) {
    398                 fileName = ""; //$NON-NLS-1$
    399             } else {
    400                 fileName = name.trim();
    401                 if (fileName.length() > 0 && fileName.indexOf('.') == -1) {
    402                     fileName = fileName + SdkConstants.DOT_XML;
    403                 }
    404             }
    405 
    406             return fileName;
    407         }
    408 
    409         /**
    410          * Returns a {@link IFile} for the destination file.
    411          * <p/>
    412          * Returns null if the project, filename or folder are invalid and the
    413          * destination file cannot be determined.
    414          * <p/>
    415          * The {@link IFile} is a resource. There might or might not be an
    416          * actual real file.
    417          *
    418          * @return an {@link IFile} for the destination file
    419          */
    420         public IFile getDestinationFile() {
    421             String fileName = getFileName();
    422             if (project != null && folderPath != null && folderPath.length() > 0
    423                     && fileName.length() > 0) {
    424                 IPath dest = new Path(folderPath).append(fileName);
    425                 IFile file = project.getFile(dest);
    426                 return file;
    427             }
    428             return null;
    429         }
    430     }
    431 }
    432