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