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