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 init(IWorkbench workbench, IStructuredSelection selection)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 */ createMainPage(NewXmlFileWizard.Values values)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 addPages()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 performFinish()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 createXmlFile()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 */ createXmlFile(IFile file, String xmlns, String root, String rootAttributes, String child, ResourceFolderType folderType)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 */ canCreateXmlFile(ResourceFolderType folderType)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 */ createXmlFile(IProject project, IFile file, ResourceFolderType folderType)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 */ setImageDescriptor()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} */ NewLayoutWizard()346 public NewLayoutWizard() { 347 } 348 349 @Override init(IWorkbench workbench, IStructuredSelection selection)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} */ NewValuesWizard()364 public NewValuesWizard() { 365 } 366 367 @Override init(IWorkbench workbench, IStructuredSelection selection)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 */ getFileName()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 */ getDestinationFile()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