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 package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; 19 20 import static com.android.AndroidConstants.RES_QUALIFIER_SEP; 21 import static com.android.ide.common.layout.LayoutConstants.HORIZONTAL_SCROLL_VIEW; 22 import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT; 23 import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW; 24 import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; 25 import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; 26 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; 27 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR; 28 import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS; 29 30 import com.android.ide.common.resources.configuration.FolderConfiguration; 31 import com.android.ide.common.resources.configuration.ResourceQualifier; 32 import com.android.ide.eclipse.adt.AdtConstants; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.AdtUtils; 35 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 36 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 37 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 38 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 39 import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; 40 import com.android.ide.eclipse.adt.internal.editors.menu.descriptors.MenuDescriptors; 41 import com.android.ide.eclipse.adt.internal.editors.values.descriptors.ValuesDescriptors; 42 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; 43 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo; 44 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; 45 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 46 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 47 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; 48 import com.android.resources.ResourceFolderType; 49 import com.android.sdklib.IAndroidTarget; 50 import com.android.sdklib.SdkConstants; 51 import com.android.util.Pair; 52 53 import org.eclipse.core.resources.IFile; 54 import org.eclipse.core.resources.IProject; 55 import org.eclipse.core.resources.IResource; 56 import org.eclipse.core.runtime.CoreException; 57 import org.eclipse.core.runtime.IAdaptable; 58 import org.eclipse.core.runtime.IPath; 59 import org.eclipse.core.runtime.IStatus; 60 import org.eclipse.jdt.core.IJavaProject; 61 import org.eclipse.jface.dialogs.IMessageProvider; 62 import org.eclipse.jface.viewers.ArrayContentProvider; 63 import org.eclipse.jface.viewers.ColumnLabelProvider; 64 import org.eclipse.jface.viewers.IBaseLabelProvider; 65 import org.eclipse.jface.viewers.IStructuredSelection; 66 import org.eclipse.jface.viewers.TableViewer; 67 import org.eclipse.jface.wizard.WizardPage; 68 import org.eclipse.swt.SWT; 69 import org.eclipse.swt.events.ModifyEvent; 70 import org.eclipse.swt.events.ModifyListener; 71 import org.eclipse.swt.events.SelectionAdapter; 72 import org.eclipse.swt.events.SelectionEvent; 73 import org.eclipse.swt.graphics.Image; 74 import org.eclipse.swt.layout.GridData; 75 import org.eclipse.swt.layout.GridLayout; 76 import org.eclipse.swt.widgets.Combo; 77 import org.eclipse.swt.widgets.Composite; 78 import org.eclipse.swt.widgets.Label; 79 import org.eclipse.swt.widgets.Table; 80 import org.eclipse.swt.widgets.Text; 81 import org.eclipse.ui.IEditorPart; 82 import org.eclipse.ui.IWorkbenchPage; 83 import org.eclipse.ui.IWorkbenchWindow; 84 import org.eclipse.ui.PlatformUI; 85 import org.eclipse.ui.part.FileEditorInput; 86 87 import java.util.ArrayList; 88 import java.util.Collections; 89 import java.util.HashSet; 90 import java.util.List; 91 92 /** 93 * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create 94 * skeleton XML resources files for Android projects. 95 * <p/> 96 * This page is used to select the project, resource type and file name. 97 */ 98 class NewXmlFileCreationPage extends WizardPage { 99 100 @Override setVisible(boolean visible)101 public void setVisible(boolean visible) { 102 super.setVisible(visible); 103 // Ensure the initial focus is in the Name field; you usually don't need 104 // to edit the default text field (the project name) 105 if (visible && mFileNameTextField != null) { 106 mFileNameTextField.setFocus(); 107 } 108 109 validatePage(); 110 } 111 112 /** 113 * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) 114 */ 115 static class TypeInfo { 116 private final String mUiName; 117 private final ResourceFolderType mResFolderType; 118 private final String mTooltip; 119 private final Object mRootSeed; 120 private ArrayList<String> mRoots = new ArrayList<String>(); 121 private final String mXmlns; 122 private final String mDefaultAttrs; 123 private final String mDefaultRoot; 124 private final int mTargetApiLevel; 125 TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)126 public TypeInfo(String uiName, 127 String tooltip, 128 ResourceFolderType resFolderType, 129 Object rootSeed, 130 String defaultRoot, 131 String xmlns, 132 String defaultAttrs, 133 int targetApiLevel) { 134 mUiName = uiName; 135 mResFolderType = resFolderType; 136 mTooltip = tooltip; 137 mRootSeed = rootSeed; 138 mDefaultRoot = defaultRoot; 139 mXmlns = xmlns; 140 mDefaultAttrs = defaultAttrs; 141 mTargetApiLevel = targetApiLevel; 142 } 143 144 /** Returns the UI name for the resource type. Unique. Never null. */ getUiName()145 String getUiName() { 146 return mUiName; 147 } 148 149 /** Returns the tooltip for the resource type. Can be null. */ getTooltip()150 String getTooltip() { 151 return mTooltip; 152 } 153 154 /** 155 * Returns the name of the {@link ResourceFolderType}. 156 * Never null but not necessarily unique, 157 * e.g. two types use {@link ResourceFolderType#XML}. 158 */ getResFolderName()159 String getResFolderName() { 160 return mResFolderType.getName(); 161 } 162 163 /** 164 * Returns the matching {@link ResourceFolderType}. 165 * Never null but not necessarily unique, 166 * e.g. two types use {@link ResourceFolderType#XML}. 167 */ getResFolderType()168 ResourceFolderType getResFolderType() { 169 return mResFolderType; 170 } 171 172 /** 173 * Returns the seed used to fill the root element values. 174 * The seed might be either a String, a String array, an {@link ElementDescriptor}, 175 * a {@link DocumentDescriptor} or null. 176 */ getRootSeed()177 Object getRootSeed() { 178 return mRootSeed; 179 } 180 181 /** 182 * Returns the default root element that should be selected by default. Can be 183 * null. 184 * 185 * @param project the associated project, or null if not known 186 */ getDefaultRoot(IProject project)187 String getDefaultRoot(IProject project) { 188 return mDefaultRoot; 189 } 190 191 /** 192 * Returns the list of all possible root elements for the resource type. 193 * This can be an empty ArrayList but not null. 194 * <p/> 195 * TODO: the root list SHOULD depend on the currently selected project, to include 196 * custom classes. 197 */ getRoots()198 ArrayList<String> getRoots() { 199 return mRoots; 200 } 201 202 /** 203 * If the generated resource XML file requires an "android" XMLNS, this should be set 204 * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. 205 */ getXmlns()206 String getXmlns() { 207 return mXmlns; 208 } 209 210 /** 211 * When not null, this represent extra attributes that must be specified in the 212 * root element of the generated XML file. When null, no extra attributes are inserted. 213 * 214 * @param project the project to get the attributes for 215 * @param root the selected root element string, never null 216 */ getDefaultAttrs(IProject project, String root)217 String getDefaultAttrs(IProject project, String root) { 218 return mDefaultAttrs; 219 } 220 221 /** 222 * When not null, represents an extra string that should be written inside 223 * the element when constructed 224 * 225 * @param project the project to get the child content for 226 * @param root the chosen root element 227 * @return a string to be written inside the root element, or null if nothing 228 */ getChild(IProject project, String root)229 String getChild(IProject project, String root) { 230 return null; 231 } 232 233 /** 234 * The minimum API level required by the current SDK target to support this feature. 235 * 236 * @return the minimum API level 237 */ getTargetApiLevel()238 public int getTargetApiLevel() { 239 return mTargetApiLevel; 240 } 241 } 242 243 /** 244 * TypeInfo, information for each "type" of file that can be created. 245 */ 246 private static final TypeInfo[] sTypes = { 247 new TypeInfo( 248 "Layout", // UI name 249 "An XML file that describes a screen layout.", // tooltip 250 ResourceFolderType.LAYOUT, // folder type 251 AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed 252 LINEAR_LAYOUT, // default root 253 SdkConstants.NS_RESOURCES, // xmlns 254 "", // not used, see below 255 1 // target API level 256 ) { 257 258 @Override 259 String getDefaultRoot(IProject project) { 260 // TODO: Use GridLayout by default for new SDKs 261 // (when we've ironed out all the usability issues) 262 //Sdk currentSdk = Sdk.getCurrent(); 263 //if (project != null && currentSdk != null) { 264 // IAndroidTarget target = currentSdk.getTarget(project); 265 // // fill_parent was renamed match_parent in API level 8 266 // if (target != null && target.getVersion().getApiLevel() >= 13) { 267 // return GRID_LAYOUT; 268 // } 269 //} 270 271 return LINEAR_LAYOUT; 272 }; 273 274 // The default attributes must be determined dynamically since whether 275 // we use match_parent or fill_parent depends on the API level of the 276 // project 277 @Override 278 String getDefaultAttrs(IProject project, String root) { 279 Sdk currentSdk = Sdk.getCurrent(); 280 String fill = VALUE_FILL_PARENT; 281 if (currentSdk != null) { 282 IAndroidTarget target = currentSdk.getTarget(project); 283 // fill_parent was renamed match_parent in API level 8 284 if (target != null && target.getVersion().getApiLevel() >= 8) { 285 fill = VALUE_MATCH_PARENT; 286 } 287 } 288 289 // Only set "vertical" orientation of LinearLayouts by default; 290 // for GridLayouts for example we want to rely on the real default 291 // of the layout 292 String size = String.format( 293 "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ 294 + "android:layout_height=\"%2$s\"", //$NON-NLS-1$ 295 fill, fill); 296 if (LINEAR_LAYOUT.equals(root)) { 297 return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$ 298 } else { 299 return size; 300 } 301 } 302 303 @Override 304 String getChild(IProject project, String root) { 305 // Create vertical linear layouts inside new scroll views 306 if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { 307 return " <LinearLayout " //$NON-NLS-1$ 308 + getDefaultAttrs(project, root).replace('\n', ' ') 309 + "></LinearLayout>\n"; //$NON-NLS-1$ 310 } 311 return null; 312 } 313 }, 314 new TypeInfo("Values", // UI name 315 "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip 316 ResourceFolderType.VALUES, // folder type 317 ValuesDescriptors.ROOT_ELEMENT, // root seed 318 null, // default root 319 null, // xmlns 320 null, // default attributes 321 1 // target API level 322 ), 323 new TypeInfo("Drawable", // UI name 324 "An XML file that describes a drawable.", // tooltip 325 ResourceFolderType.DRAWABLE, // folder type 326 AndroidTargetData.DESCRIPTOR_DRAWABLE, // root seed 327 null, // default root 328 SdkConstants.NS_RESOURCES, // xmlns 329 null, // default attributes 330 1 // target API level 331 ), 332 new TypeInfo("Menu", // UI name 333 "An XML file that describes an menu.", // tooltip 334 ResourceFolderType.MENU, // folder type 335 MenuDescriptors.MENU_ROOT_ELEMENT, // root seed 336 null, // default root 337 SdkConstants.NS_RESOURCES, // xmlns 338 null, // default attributes 339 1 // target API level 340 ), 341 new TypeInfo("Color List", // UI name 342 "An XML file that describes a color state list.", // tooltip 343 ResourceFolderType.COLOR, // folder type 344 AndroidTargetData.DESCRIPTOR_COLOR, // root seed 345 "selector", //$NON-NLS-1$ // default root 346 SdkConstants.NS_RESOURCES, // xmlns 347 null, // default attributes 348 1 // target API level 349 ), 350 new TypeInfo("Property Animation", // UI name 351 "An XML file that describes a property animation", // tooltip 352 ResourceFolderType.ANIMATOR, // folder type 353 AndroidTargetData.DESCRIPTOR_ANIMATOR, // root seed 354 "set", //$NON-NLS-1$ // default root 355 SdkConstants.NS_RESOURCES, // xmlns 356 null, // default attributes 357 11 // target API level 358 ), 359 new TypeInfo("Tween Animation", // UI name 360 "An XML file that describes a tween animation.", // tooltip 361 ResourceFolderType.ANIM, // folder type 362 AndroidTargetData.DESCRIPTOR_ANIM, // root seed 363 "set", //$NON-NLS-1$ // default root 364 null, // xmlns 365 null, // default attributes 366 1 // target API level 367 ), 368 new TypeInfo("AppWidget Provider", // UI name 369 "An XML file that describes a widget provider.", // tooltip 370 ResourceFolderType.XML, // folder type 371 AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed 372 null, // default root 373 SdkConstants.NS_RESOURCES, // xmlns 374 null, // default attributes 375 3 // target API level 376 ), 377 new TypeInfo("Preference", // UI name 378 "An XML file that describes preferences.", // tooltip 379 ResourceFolderType.XML, // folder type 380 AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed 381 SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root 382 SdkConstants.NS_RESOURCES, // xmlns 383 null, // default attributes 384 1 // target API level 385 ), 386 new TypeInfo("Searchable", // UI name 387 "An XML file that describes a searchable.", // tooltip 388 ResourceFolderType.XML, // folder type 389 AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed 390 null, // default root 391 SdkConstants.NS_RESOURCES, // xmlns 392 null, // default attributes 393 1 // target API level 394 ), 395 // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in 396 // this menu since it's not often used for creating XML files. 397 }; 398 399 private NewXmlFileWizard.Values mValues; 400 private ProjectCombo mProjectButton; 401 private Text mFileNameTextField; 402 private Combo mTypeCombo; 403 private IStructuredSelection mInitialSelection; 404 private ResourceFolderType mInitialFolderType; 405 private boolean mInternalTypeUpdate; 406 private TargetChangeListener mSdkTargetChangeListener; 407 private Table mRootTable; 408 private TableViewer mRootTableViewer; 409 410 // --- UI creation --- 411 412 /** 413 * Constructs a new {@link NewXmlFileCreationPage}. 414 * <p/> 415 * Called by {@link NewXmlFileWizard#createMainPage}. 416 */ NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values)417 protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { 418 super(pageName); 419 mValues = values; 420 setPageComplete(false); 421 } 422 setInitialSelection(IStructuredSelection initialSelection)423 public void setInitialSelection(IStructuredSelection initialSelection) { 424 mInitialSelection = initialSelection; 425 } 426 setInitialFolderType(ResourceFolderType initialType)427 public void setInitialFolderType(ResourceFolderType initialType) { 428 mInitialFolderType = initialType; 429 } 430 431 /** 432 * Called by the parent Wizard to create the UI for this Wizard Page. 433 * 434 * {@inheritDoc} 435 * 436 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) 437 */ 438 @Override 439 @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused createControl(Composite parent)440 public void createControl(Composite parent) { 441 // This UI is maintained with WindowBuilder. 442 443 Composite composite = new Composite(parent, SWT.NULL); 444 composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/)); 445 composite.setLayoutData(new GridData(GridData.FILL_BOTH)); 446 447 // label before type radios 448 Label typeLabel = new Label(composite, SWT.NONE); 449 typeLabel.setText("Resource Type:"); 450 451 mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY); 452 mTypeCombo.setToolTipText("What type of resource would you like to create?"); 453 mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 454 if (mInitialFolderType != null) { 455 mTypeCombo.setEnabled(false); 456 } 457 mTypeCombo.addSelectionListener(new SelectionAdapter() { 458 @Override 459 public void widgetSelected(SelectionEvent e) { 460 TypeInfo type = getSelectedType(); 461 if (type != null) { 462 onSelectType(type); 463 } 464 } 465 }); 466 467 // separator 468 Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); 469 GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL); 470 gd2.horizontalAlignment = SWT.FILL; 471 gd2.horizontalSpan = 2; 472 separator.setLayoutData(gd2); 473 474 // Project: [button] 475 String tooltip = "The Android Project where the new resource file will be created."; 476 Label projectLabel = new Label(composite, SWT.NONE); 477 projectLabel.setText("Project:"); 478 projectLabel.setToolTipText(tooltip); 479 480 ProjectChooserHelper helper = 481 new ProjectChooserHelper(getShell(), null /* filter */); 482 483 mProjectButton = new ProjectCombo(helper, composite, mValues.project); 484 mProjectButton.setToolTipText(tooltip); 485 mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 486 mProjectButton.addSelectionListener(new SelectionAdapter() { 487 @Override 488 public void widgetSelected(SelectionEvent e) { 489 IProject project = mProjectButton.getSelectedProject(); 490 if (project != mValues.project) { 491 changeProject(project); 492 } 493 }; 494 }); 495 496 // Filename: [text] 497 Label fileLabel = new Label(composite, SWT.NONE); 498 fileLabel.setText("File:"); 499 fileLabel.setToolTipText("The name of the resource file to create."); 500 501 mFileNameTextField = new Text(composite, SWT.BORDER); 502 mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 503 mFileNameTextField.setToolTipText(tooltip); 504 mFileNameTextField.addModifyListener(new ModifyListener() { 505 @Override 506 public void modifyText(ModifyEvent e) { 507 mValues.name = mFileNameTextField.getText(); 508 validatePage(); 509 } 510 }); 511 512 // separator 513 Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); 514 GridData gd = new GridData(GridData.GRAB_HORIZONTAL); 515 gd.horizontalAlignment = SWT.FILL; 516 gd.horizontalSpan = 2; 517 rootSeparator.setLayoutData(gd); 518 519 // Root Element: 520 // [TableViewer] 521 Label rootLabel = new Label(composite, SWT.NONE); 522 rootLabel.setText("Root Element:"); 523 new Label(composite, SWT.NONE); 524 525 mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION); 526 mRootTable = mRootTableViewer.getTable(); 527 GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); 528 tableGridData.heightHint = 200; 529 mRootTable.setLayoutData(tableGridData); 530 531 setControl(composite); 532 533 // Update state the first time 534 setErrorMessage(null); 535 setMessage(null); 536 537 initializeFromSelection(mInitialSelection); 538 updateAvailableTypes(); 539 initializeFromFixedType(); 540 initializeRootValues(); 541 installTargetChangeListener(); 542 543 initialSelectType(); 544 validatePage(); 545 } 546 initialSelectType()547 private void initialSelectType() { 548 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 549 int typeIndex = getTypeComboIndex(mValues.type); 550 if (typeIndex == -1) { 551 typeIndex = 0; 552 } else { 553 assert mValues.type == types[typeIndex]; 554 } 555 mTypeCombo.select(typeIndex); 556 onSelectType(types[typeIndex]); 557 updateRootCombo(types[typeIndex]); 558 } 559 installTargetChangeListener()560 private void installTargetChangeListener() { 561 mSdkTargetChangeListener = new TargetChangeListener() { 562 @Override 563 public IProject getProject() { 564 return mValues.project; 565 } 566 567 @Override 568 public void reload() { 569 if (mValues.project != null) { 570 changeProject(mValues.project); 571 } 572 } 573 }; 574 575 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); 576 } 577 578 @Override dispose()579 public void dispose() { 580 581 if (mSdkTargetChangeListener != null) { 582 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); 583 mSdkTargetChangeListener = null; 584 } 585 586 super.dispose(); 587 } 588 589 /** 590 * Returns the selected root element string, if any. 591 * 592 * @return The selected root element string or null. 593 */ getRootElement()594 public String getRootElement() { 595 int index = mRootTable.getSelectionIndex(); 596 if (index >= 0) { 597 Object[] roots = (Object[]) mRootTableViewer.getInput(); 598 return roots[index].toString(); 599 } 600 return null; 601 } 602 603 /** 604 * Called by {@link NewXmlFileWizard} to initialize the page with the selection 605 * received by the wizard -- typically the current user workbench selection. 606 * <p/> 607 * Things we expect to find out from the selection: 608 * <ul> 609 * <li>The project name, valid if it's an android nature.</li> 610 * <li>The current folder, valid if it's a folder under /res</li> 611 * <li>An existing filename, in which case the user will be asked whether to override it.</li> 612 * </ul> 613 * <p/> 614 * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace 615 * resource path (where the resource path does not have to exist yet, such as res/anim/). 616 * 617 * @param selection The selection when the wizard was initiated. 618 */ initializeFromSelection(IStructuredSelection selection)619 private boolean initializeFromSelection(IStructuredSelection selection) { 620 if (selection == null) { 621 return false; 622 } 623 624 // Find the best match in the element list. In case there are multiple selected elements 625 // select the one that provides the most information and assign them a score, 626 // e.g. project=1 + folder=2 + file=4. 627 IProject targetProject = null; 628 String targetWsFolderPath = null; 629 String targetFileName = null; 630 int targetScore = 0; 631 for (Object element : selection.toList()) { 632 if (element instanceof IAdaptable) { 633 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); 634 IProject project = res != null ? res.getProject() : null; 635 636 // Is this an Android project? 637 try { 638 if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) { 639 continue; 640 } 641 } catch (CoreException e) { 642 // checking the nature failed, ignore this resource 643 continue; 644 } 645 646 int score = 1; // we have a valid project at least 647 648 IPath wsFolderPath = null; 649 String fileName = null; 650 assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no 651 if (res.getType() == IResource.FOLDER) { 652 wsFolderPath = res.getProjectRelativePath(); 653 } else if (res.getType() == IResource.FILE) { 654 if (AdtUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) { 655 fileName = res.getName(); 656 } 657 wsFolderPath = res.getParent().getProjectRelativePath(); 658 } 659 660 // Disregard this folder selection if it doesn't point to /res/something 661 if (wsFolderPath != null && 662 wsFolderPath.segmentCount() > 1 && 663 SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { 664 score += 2; 665 } else { 666 wsFolderPath = null; 667 fileName = null; 668 } 669 670 score += fileName != null ? 4 : 0; 671 672 if (score > targetScore) { 673 targetScore = score; 674 targetProject = project; 675 targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; 676 targetFileName = fileName; 677 } 678 } else if (element instanceof Pair<?,?>) { 679 // Pair of Project/String 680 @SuppressWarnings("unchecked") 681 Pair<IProject,String> pair = (Pair<IProject,String>)element; 682 targetScore = 1; 683 targetProject = pair.getFirst(); 684 targetWsFolderPath = pair.getSecond(); 685 targetFileName = ""; 686 } 687 } 688 689 if (targetProject == null) { 690 // Try to figure out the project from the active editor 691 IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); 692 if (window != null) { 693 IWorkbenchPage page = window.getActivePage(); 694 if (page != null) { 695 IEditorPart activeEditor = page.getActiveEditor(); 696 if (activeEditor instanceof AndroidXmlEditor) { 697 Object input = ((AndroidXmlEditor) activeEditor).getEditorInput(); 698 if (input instanceof FileEditorInput) { 699 FileEditorInput fileInput = (FileEditorInput) input; 700 targetScore = 1; 701 IFile file = fileInput.getFile(); 702 targetProject = file.getProject(); 703 IPath path = file.getParent().getProjectRelativePath(); 704 targetWsFolderPath = path != null ? path.toString() : null; 705 } 706 } 707 } 708 } 709 } 710 711 if (targetProject == null) { 712 // If we didn't find a default project based on the selection, check how many 713 // open Android projects we can find in the current workspace. If there's only 714 // one, we'll just select it by default. 715 IJavaProject[] projects = AdtUtils.getOpenAndroidProjects(); 716 if (projects != null && projects.length == 1) { 717 targetScore = 1; 718 targetProject = projects[0].getProject(); 719 } 720 } 721 722 // Now set the UI accordingly 723 if (targetScore > 0) { 724 mValues.project = targetProject; 725 mValues.folderPath = targetWsFolderPath; 726 mProjectButton.setSelectedProject(targetProject); 727 mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ 728 729 // If the current selection context corresponds to a specific file type, 730 // select it. 731 if (targetWsFolderPath != null) { 732 int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR); 733 if (pos >= 0) { 734 targetWsFolderPath = targetWsFolderPath.substring(pos + 1); 735 } 736 String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP); 737 if (folderSegments.length > 0) { 738 String folderName = folderSegments[0]; 739 selectTypeFromFolder(folderName); 740 } 741 } 742 } 743 744 return true; 745 } 746 initializeFromFixedType()747 private void initializeFromFixedType() { 748 if (mInitialFolderType != null) { 749 for (TypeInfo type : sTypes) { 750 if (type.getResFolderType() == mInitialFolderType) { 751 mValues.type = type; 752 updateFolderPath(type); 753 break; 754 } 755 } 756 } 757 } 758 759 /** 760 * Given a folder name, such as "drawable", select the corresponding type in 761 * the dropdown. 762 */ selectTypeFromFolder(String folderName)763 void selectTypeFromFolder(String folderName) { 764 List<TypeInfo> matches = new ArrayList<TypeInfo>(); 765 boolean selected = false; 766 767 TypeInfo selectedType = getSelectedType(); 768 for (TypeInfo type : sTypes) { 769 if (type.getResFolderName().equals(folderName)) { 770 matches.add(type); 771 selected |= type == selectedType; 772 } 773 } 774 775 if (matches.size() == 1) { 776 // If there's only one match, select it if it's not already selected 777 if (!selected) { 778 selectType(matches.get(0)); 779 } 780 } else if (matches.size() > 1) { 781 // There are multiple type candidates for this folder. This can happen 782 // for /res/xml for example. Check to see if one of them is currently 783 // selected. If yes, leave the selection unchanged. If not, deselect all type. 784 if (!selected) { 785 selectType(null); 786 } 787 } else { 788 // Nothing valid was selected. 789 selectType(null); 790 } 791 } 792 793 /** 794 * Initialize the root values of the type infos based on the current framework values. 795 */ initializeRootValues()796 private void initializeRootValues() { 797 IProject project = mValues.project; 798 for (TypeInfo type : sTypes) { 799 // Clear all the roots for this type 800 ArrayList<String> roots = type.getRoots(); 801 if (roots.size() > 0) { 802 roots.clear(); 803 } 804 805 // depending of the type of the seed, initialize the root in different ways 806 Object rootSeed = type.getRootSeed(); 807 808 if (rootSeed instanceof String) { 809 // The seed is a single string, Add it as-is. 810 roots.add((String) rootSeed); 811 } else if (rootSeed instanceof String[]) { 812 // The seed is an array of strings. Add them as-is. 813 for (String value : (String[]) rootSeed) { 814 roots.add(value); 815 } 816 } else if (rootSeed instanceof Integer && project != null) { 817 // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_* 818 // In this case add all the children element descriptors defined, recursively, 819 // and avoid infinite recursion by keeping track of what has already been added. 820 821 // Note: if project is null, the root list will be empty since it has been 822 // cleared above. 823 824 // get the AndroidTargetData from the project 825 IAndroidTarget target = null; 826 AndroidTargetData data = null; 827 828 target = Sdk.getCurrent().getTarget(project); 829 if (target == null) { 830 // A project should have a target. The target can be missing if the project 831 // is an old project for which a target hasn't been affected or if the 832 // target no longer exists in this SDK. Simply log the error and dismiss. 833 834 AdtPlugin.log(IStatus.INFO, 835 "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$ 836 project.getName()); 837 continue; 838 } else { 839 data = Sdk.getCurrent().getTargetData(target); 840 841 if (data == null) { 842 // We should have both a target and its data. 843 // However if the wizard is invoked whilst the platform is still being 844 // loaded we can end up in a weird case where we have a target but it 845 // doesn't have any data yet. 846 // Lets log a warning and silently ignore this root. 847 848 AdtPlugin.log(IStatus.INFO, 849 "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$ 850 target.getName(), project.getName()); 851 continue; 852 } 853 } 854 855 IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); 856 ElementDescriptor descriptor = provider.getDescriptor(); 857 if (descriptor != null) { 858 HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>(); 859 initRootElementDescriptor(roots, descriptor, visited); 860 } 861 862 // Sort alphabetically. 863 Collections.sort(roots); 864 } 865 } 866 } 867 868 /** 869 * Helper method to recursively insert all XML names for the given {@link ElementDescriptor} 870 * into the roots array list. Keeps track of visited nodes to avoid infinite recursion. 871 * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic 872 * and not a valid root element. 873 */ initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited)874 private void initRootElementDescriptor(ArrayList<String> roots, 875 ElementDescriptor desc, HashSet<ElementDescriptor> visited) { 876 if (!(desc instanceof DocumentDescriptor)) { 877 String xmlName = desc.getXmlName(); 878 if (xmlName != null && xmlName.length() > 0) { 879 roots.add(xmlName); 880 } 881 } 882 883 visited.add(desc); 884 885 for (ElementDescriptor child : desc.getChildren()) { 886 if (!visited.contains(child)) { 887 initRootElementDescriptor(roots, child, visited); 888 } 889 } 890 } 891 892 /** 893 * Changes mProject to the given new project and update the UI accordingly. 894 * <p/> 895 * Note that this does not check if the new project is the same as the current one 896 * on purpose, which allows a project to be updated when its target has changed or 897 * when targets are loaded in the background. 898 */ changeProject(IProject newProject)899 private void changeProject(IProject newProject) { 900 mValues.project = newProject; 901 902 // enable types based on new API level 903 updateAvailableTypes(); 904 initialSelectType(); 905 906 // update the folder name based on API level 907 updateFolderPath(mValues.type); 908 909 // update the Type with the new descriptors. 910 initializeRootValues(); 911 912 // update the combo 913 updateRootCombo(mValues.type); 914 915 validatePage(); 916 } 917 onSelectType(TypeInfo type)918 private void onSelectType(TypeInfo type) { 919 // Do nothing if this is an internal modification or if the widget has been 920 // deselected. 921 if (mInternalTypeUpdate) { 922 return; 923 } 924 925 mValues.type = type; 926 927 if (type == null) { 928 return; 929 } 930 931 // update the combo 932 updateRootCombo(type); 933 934 // update the folder path 935 updateFolderPath(type); 936 937 validatePage(); 938 } 939 940 /** Updates the selected type in the type dropdown control */ setSelectedType(TypeInfo type)941 private void setSelectedType(TypeInfo type) { 942 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 943 if (types != null) { 944 for (int i = 0, n = types.length; i < n; i++) { 945 if (types[i] == type) { 946 mTypeCombo.select(i); 947 break; 948 } 949 } 950 } 951 } 952 953 /** Returns the selected type in the type dropdown control */ getSelectedType()954 private TypeInfo getSelectedType() { 955 int index = mTypeCombo.getSelectionIndex(); 956 if (index != -1) { 957 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 958 return types[index]; 959 } 960 961 return null; 962 } 963 964 /** Returns the selected index in the type dropdown control */ getTypeComboIndex(TypeInfo type)965 private int getTypeComboIndex(TypeInfo type) { 966 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 967 for (int i = 0, n = types.length; i < n; i++) { 968 if (type == types[i]) { 969 return i; 970 } 971 } 972 973 return -1; 974 } 975 976 /** Updates the folder path to reflect the given type */ updateFolderPath(TypeInfo type)977 private void updateFolderPath(TypeInfo type) { 978 String wsFolderPath = mValues.folderPath; 979 String newPath = null; 980 FolderConfiguration config = mValues.configuration; 981 ResourceQualifier qual = config.getInvalidQualifier(); 982 if (qual == null) { 983 // The configuration is valid. Reformat the folder path using the canonical 984 // value from the configuration. 985 newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); 986 } else { 987 // The configuration is invalid. We still update the path but this time 988 // do it manually on the string. 989 if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { 990 wsFolderPath = wsFolderPath.replaceFirst( 991 "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$ 992 "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$ 993 } else { 994 newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); 995 } 996 } 997 998 if (newPath != null && !newPath.equals(wsFolderPath)) { 999 mValues.folderPath = newPath; 1000 } 1001 } 1002 1003 /** 1004 * Helper method that fills the values of the "root element" combo box based 1005 * on the currently selected type radio button. Also disables the combo is there's 1006 * only one choice. Always select the first root element for the given type. 1007 * 1008 * @param type The currently selected {@link TypeInfo}, or null 1009 */ updateRootCombo(TypeInfo type)1010 private void updateRootCombo(TypeInfo type) { 1011 IBaseLabelProvider labelProvider = new ColumnLabelProvider() { 1012 @Override 1013 public Image getImage(Object element) { 1014 return IconFactory.getInstance().getIcon(element.toString()); 1015 } 1016 }; 1017 mRootTableViewer.setContentProvider(new ArrayContentProvider()); 1018 mRootTableViewer.setLabelProvider(labelProvider); 1019 1020 if (type != null) { 1021 // get the list of roots. The list can be empty but not null. 1022 ArrayList<String> roots = type.getRoots(); 1023 mRootTableViewer.setInput(roots.toArray()); 1024 1025 int index = 0; // default is to select the first one 1026 String defaultRoot = type.getDefaultRoot(mValues.project); 1027 if (defaultRoot != null) { 1028 index = roots.indexOf(defaultRoot); 1029 } 1030 mRootTable.select(index < 0 ? 0 : index); 1031 mRootTable.showSelection(); 1032 } 1033 } 1034 1035 /** 1036 * Helper method to select the current type in the type dropdown 1037 * 1038 * @param type The TypeInfo matching the radio button to selected or null to deselect them all. 1039 */ 1040 private void selectType(TypeInfo type) { 1041 mInternalTypeUpdate = true; 1042 mValues.type = type; 1043 if (type == null) { 1044 if (mTypeCombo.getSelectionIndex() != -1) { 1045 mTypeCombo.deselect(mTypeCombo.getSelectionIndex()); 1046 } 1047 } else { 1048 setSelectedType(type); 1049 } 1050 updateRootCombo(type); 1051 mInternalTypeUpdate = false; 1052 } 1053 1054 /** 1055 * Add the available types in the type combobox, based on whether they are available 1056 * for the current SDK. 1057 * <p/> 1058 * A type is available either if: 1059 * - if mProject is null, API level 1 is considered valid 1060 * - if mProject is !null, the project->target->API must be >= to the type's API level. 1061 */ 1062 private void updateAvailableTypes() { 1063 IProject project = mValues.project; 1064 IAndroidTarget target = project != null ? Sdk.getCurrent().getTarget(project) : null; 1065 int currentApiLevel = 1; 1066 if (target != null) { 1067 currentApiLevel = target.getVersion().getApiLevel(); 1068 } 1069 1070 List<String> items = new ArrayList<String>(sTypes.length); 1071 List<TypeInfo> types = new ArrayList<TypeInfo>(sTypes.length); 1072 for (int i = 0, n = sTypes.length; i < n; i++) { 1073 TypeInfo type = sTypes[i]; 1074 if (type.getTargetApiLevel() <= currentApiLevel) { 1075 items.add(type.getUiName()); 1076 types.add(type); 1077 } 1078 } 1079 mTypeCombo.setItems(items.toArray(new String[items.size()])); 1080 mTypeCombo.setData(types.toArray(new TypeInfo[types.size()])); 1081 } 1082 1083 /** 1084 * Validates the fields, displays errors and warnings. 1085 * Enables the finish button if there are no errors. 1086 */ 1087 private void validatePage() { 1088 String error = null; 1089 String warning = null; 1090 1091 // -- validate type 1092 TypeInfo type = mValues.type; 1093 if (error == null) { 1094 if (type == null) { 1095 error = "One of the types must be selected (e.g. layout, values, etc.)"; 1096 } 1097 } 1098 1099 // -- validate project 1100 if (mValues.project == null) { 1101 error = "Please select an Android project."; 1102 } 1103 1104 // -- validate type API level 1105 if (error == null) { 1106 IAndroidTarget target = Sdk.getCurrent().getTarget(mValues.project); 1107 int currentApiLevel = 1; 1108 if (target != null) { 1109 currentApiLevel = target.getVersion().getApiLevel(); 1110 } 1111 1112 assert type != null; 1113 if (type.getTargetApiLevel() > currentApiLevel) { 1114 error = "The API level of the selected type (e.g. AppWidget, etc.) is not " + 1115 "compatible with the API level of the project."; 1116 } 1117 } 1118 1119 // -- validate filename 1120 if (error == null) { 1121 String fileName = mValues.getFileName(); 1122 assert type != null; 1123 ResourceFolderType folderType = type.getResFolderType(); 1124 error = ResourceNameValidator.create(true, folderType).isValid(fileName); 1125 } 1126 1127 // -- validate destination file doesn't exist 1128 if (error == null) { 1129 IFile file = mValues.getDestinationFile(); 1130 if (file != null && file.exists()) { 1131 warning = "The destination file already exists"; 1132 } 1133 } 1134 1135 // -- update UI & enable finish if there's no error 1136 setPageComplete(error == null); 1137 if (error != null) { 1138 setMessage(error, IMessageProvider.ERROR); 1139 } else if (warning != null) { 1140 setMessage(warning, IMessageProvider.WARNING); 1141 } else { 1142 setErrorMessage(null); 1143 setMessage(null); 1144 } 1145 } 1146 1147 /** 1148 * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if 1149 * not found 1150 * 1151 * @param folderType the {@link ResourceFolderType} to look for 1152 * @return the corresponding {@link TypeInfo} 1153 */ 1154 static TypeInfo getTypeInfo(ResourceFolderType folderType) { 1155 for (TypeInfo typeInfo : sTypes) { 1156 if (typeInfo.getResFolderType() == folderType) { 1157 return typeInfo; 1158 } 1159 } 1160 1161 return null; 1162 } 1163 } 1164