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.DOT_XML; 20 import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; 21 import static com.android.SdkConstants.LINEAR_LAYOUT; 22 import static com.android.SdkConstants.RES_QUALIFIER_SEP; 23 import static com.android.SdkConstants.SCROLL_VIEW; 24 import static com.android.SdkConstants.VALUE_FILL_PARENT; 25 import static com.android.SdkConstants.VALUE_MATCH_PARENT; 26 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR; 27 import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS; 28 29 import com.android.SdkConstants; 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.project.ProjectChooserHelper; 41 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo; 42 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; 43 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 44 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 45 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; 46 import com.android.resources.ResourceFolderType; 47 import com.android.sdklib.IAndroidTarget; 48 import com.android.utils.Pair; 49 import com.android.utils.SdkUtils; 50 51 import org.eclipse.core.resources.IFile; 52 import org.eclipse.core.resources.IProject; 53 import org.eclipse.core.resources.IResource; 54 import org.eclipse.core.runtime.CoreException; 55 import org.eclipse.core.runtime.IAdaptable; 56 import org.eclipse.core.runtime.IPath; 57 import org.eclipse.core.runtime.IStatus; 58 import org.eclipse.jdt.core.IJavaProject; 59 import org.eclipse.jface.dialogs.IMessageProvider; 60 import org.eclipse.jface.viewers.ArrayContentProvider; 61 import org.eclipse.jface.viewers.ColumnLabelProvider; 62 import org.eclipse.jface.viewers.IBaseLabelProvider; 63 import org.eclipse.jface.viewers.IStructuredSelection; 64 import org.eclipse.jface.viewers.TableViewer; 65 import org.eclipse.jface.wizard.WizardPage; 66 import org.eclipse.swt.SWT; 67 import org.eclipse.swt.events.ModifyEvent; 68 import org.eclipse.swt.events.ModifyListener; 69 import org.eclipse.swt.events.SelectionAdapter; 70 import org.eclipse.swt.events.SelectionEvent; 71 import org.eclipse.swt.graphics.Image; 72 import org.eclipse.swt.layout.GridData; 73 import org.eclipse.swt.layout.GridLayout; 74 import org.eclipse.swt.widgets.Combo; 75 import org.eclipse.swt.widgets.Composite; 76 import org.eclipse.swt.widgets.Label; 77 import org.eclipse.swt.widgets.Table; 78 import org.eclipse.swt.widgets.Text; 79 import org.eclipse.ui.IEditorPart; 80 import org.eclipse.ui.IWorkbenchPage; 81 import org.eclipse.ui.IWorkbenchWindow; 82 import org.eclipse.ui.PlatformUI; 83 import org.eclipse.ui.part.FileEditorInput; 84 85 import java.util.ArrayList; 86 import java.util.Collections; 87 import java.util.HashSet; 88 import java.util.List; 89 90 /** 91 * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create 92 * skeleton XML resources files for Android projects. 93 * <p/> 94 * This page is used to select the project, resource type and file name. 95 */ 96 class NewXmlFileCreationPage extends WizardPage { 97 98 @Override 99 public void setVisible(boolean visible) { 100 super.setVisible(visible); 101 // Ensure the initial focus is in the Name field; you usually don't need 102 // to edit the default text field (the project name) 103 if (visible && mFileNameTextField != null) { 104 mFileNameTextField.setFocus(); 105 } 106 107 validatePage(); 108 } 109 110 /** 111 * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) 112 */ 113 static class TypeInfo { 114 private final String mUiName; 115 private final ResourceFolderType mResFolderType; 116 private final String mTooltip; 117 private final Object mRootSeed; 118 private ArrayList<String> mRoots = new ArrayList<String>(); 119 private final String mXmlns; 120 private final String mDefaultAttrs; 121 private final String mDefaultRoot; 122 private final int mTargetApiLevel; 123 124 public TypeInfo(String uiName, 125 String tooltip, 126 ResourceFolderType resFolderType, 127 Object rootSeed, 128 String defaultRoot, 129 String xmlns, 130 String defaultAttrs, 131 int targetApiLevel) { 132 mUiName = uiName; 133 mResFolderType = resFolderType; 134 mTooltip = tooltip; 135 mRootSeed = rootSeed; 136 mDefaultRoot = defaultRoot; 137 mXmlns = xmlns; 138 mDefaultAttrs = defaultAttrs; 139 mTargetApiLevel = targetApiLevel; 140 } 141 142 /** Returns the UI name for the resource type. Unique. Never null. */ 143 String getUiName() { 144 return mUiName; 145 } 146 147 /** Returns the tooltip for the resource type. Can be null. */ 148 String getTooltip() { 149 return mTooltip; 150 } 151 152 /** 153 * Returns the name of the {@link ResourceFolderType}. 154 * Never null but not necessarily unique, 155 * e.g. two types use {@link ResourceFolderType#XML}. 156 */ 157 String getResFolderName() { 158 return mResFolderType.getName(); 159 } 160 161 /** 162 * Returns the matching {@link ResourceFolderType}. 163 * Never null but not necessarily unique, 164 * e.g. two types use {@link ResourceFolderType#XML}. 165 */ 166 ResourceFolderType getResFolderType() { 167 return mResFolderType; 168 } 169 170 /** 171 * Returns the seed used to fill the root element values. 172 * The seed might be either a String, a String array, an {@link ElementDescriptor}, 173 * a {@link DocumentDescriptor} or null. 174 */ 175 Object getRootSeed() { 176 return mRootSeed; 177 } 178 179 /** 180 * Returns the default root element that should be selected by default. Can be 181 * null. 182 * 183 * @param project the associated project, or null if not known 184 */ 185 String getDefaultRoot(IProject project) { 186 return mDefaultRoot; 187 } 188 189 /** 190 * Returns the list of all possible root elements for the resource type. 191 * This can be an empty ArrayList but not null. 192 * <p/> 193 * TODO: the root list SHOULD depend on the currently selected project, to include 194 * custom classes. 195 */ 196 ArrayList<String> getRoots() { 197 return mRoots; 198 } 199 200 /** 201 * If the generated resource XML file requires an "android" XMLNS, this should be set 202 * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. 203 */ 204 String getXmlns() { 205 return mXmlns; 206 } 207 208 /** 209 * When not null, this represent extra attributes that must be specified in the 210 * root element of the generated XML file. When null, no extra attributes are inserted. 211 * 212 * @param project the project to get the attributes for 213 * @param root the selected root element string, never null 214 */ 215 String getDefaultAttrs(IProject project, String root) { 216 return mDefaultAttrs; 217 } 218 219 /** 220 * When not null, represents an extra string that should be written inside 221 * the element when constructed 222 * 223 * @param project the project to get the child content for 224 * @param root the chosen root element 225 * @return a string to be written inside the root element, or null if nothing 226 */ 227 String getChild(IProject project, String root) { 228 return null; 229 } 230 231 /** 232 * The minimum API level required by the current SDK target to support this feature. 233 * 234 * @return the minimum API level 235 */ 236 public int getTargetApiLevel() { 237 return mTargetApiLevel; 238 } 239 } 240 241 /** 242 * TypeInfo, information for each "type" of file that can be created. 243 */ 244 private static final TypeInfo[] sTypes = { 245 new TypeInfo( 246 "Layout", // UI name 247 "An XML file that describes a screen layout.", // tooltip 248 ResourceFolderType.LAYOUT, // folder type 249 AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed 250 LINEAR_LAYOUT, // default root 251 SdkConstants.NS_RESOURCES, // xmlns 252 "", // not used, see below 253 1 // target API level 254 ) { 255 256 @Override 257 String getDefaultRoot(IProject project) { 258 // TODO: Use GridLayout by default for new SDKs 259 // (when we've ironed out all the usability issues) 260 //Sdk currentSdk = Sdk.getCurrent(); 261 //if (project != null && currentSdk != null) { 262 // IAndroidTarget target = currentSdk.getTarget(project); 263 // // fill_parent was renamed match_parent in API level 8 264 // if (target != null && target.getVersion().getApiLevel() >= 13) { 265 // return GRID_LAYOUT; 266 // } 267 //} 268 269 return LINEAR_LAYOUT; 270 }; 271 272 // The default attributes must be determined dynamically since whether 273 // we use match_parent or fill_parent depends on the API level of the 274 // project 275 @Override 276 String getDefaultAttrs(IProject project, String root) { 277 Sdk currentSdk = Sdk.getCurrent(); 278 String fill = VALUE_FILL_PARENT; 279 if (currentSdk != null) { 280 IAndroidTarget target = currentSdk.getTarget(project); 281 // fill_parent was renamed match_parent in API level 8 282 if (target != null && target.getVersion().getApiLevel() >= 8) { 283 fill = VALUE_MATCH_PARENT; 284 } 285 } 286 287 // Only set "vertical" orientation of LinearLayouts by default; 288 // for GridLayouts for example we want to rely on the real default 289 // of the layout 290 String size = String.format( 291 "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ 292 + "android:layout_height=\"%2$s\"", //$NON-NLS-1$ 293 fill, fill); 294 if (LINEAR_LAYOUT.equals(root)) { 295 return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$ 296 } else { 297 return size; 298 } 299 } 300 301 @Override 302 String getChild(IProject project, String root) { 303 // Create vertical linear layouts inside new scroll views 304 if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { 305 return " <LinearLayout " //$NON-NLS-1$ 306 + getDefaultAttrs(project, root).replace('\n', ' ') 307 + " android:orientation=\"vertical\"" //$NON-NLS-1$ 308 + "></LinearLayout>\n"; //$NON-NLS-1$ 309 } 310 return null; 311 } 312 }, 313 new TypeInfo("Values", // UI name 314 "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip 315 ResourceFolderType.VALUES, // folder type 316 SdkConstants.TAG_RESOURCES, // root seed 317 null, // default root 318 null, // xmlns 319 null, // default attributes 320 1 // target API level 321 ), 322 new TypeInfo("Drawable", // UI name 323 "An XML file that describes a drawable.", // tooltip 324 ResourceFolderType.DRAWABLE, // folder type 325 AndroidTargetData.DESCRIPTOR_DRAWABLE, // root seed 326 null, // default root 327 SdkConstants.NS_RESOURCES, // xmlns 328 null, // default attributes 329 1 // target API level 330 ), 331 new TypeInfo("Menu", // UI name 332 "An XML file that describes an menu.", // tooltip 333 ResourceFolderType.MENU, // folder type 334 SdkConstants.TAG_MENU, // root seed 335 null, // default root 336 SdkConstants.NS_RESOURCES, // xmlns 337 null, // default attributes 338 1 // target API level 339 ), 340 new TypeInfo("Color List", // UI name 341 "An XML file that describes a color state list.", // tooltip 342 ResourceFolderType.COLOR, // folder type 343 AndroidTargetData.DESCRIPTOR_COLOR, // root seed 344 "selector", //$NON-NLS-1$ // default root 345 SdkConstants.NS_RESOURCES, // xmlns 346 null, // default attributes 347 1 // target API level 348 ), 349 new TypeInfo("Property Animation", // UI name 350 "An XML file that describes a property animation", // tooltip 351 ResourceFolderType.ANIMATOR, // folder type 352 AndroidTargetData.DESCRIPTOR_ANIMATOR, // root seed 353 "set", //$NON-NLS-1$ // default root 354 SdkConstants.NS_RESOURCES, // xmlns 355 null, // default attributes 356 11 // target API level 357 ), 358 new TypeInfo("Tween Animation", // UI name 359 "An XML file that describes a tween animation.", // tooltip 360 ResourceFolderType.ANIM, // folder type 361 AndroidTargetData.DESCRIPTOR_ANIM, // root seed 362 "set", //$NON-NLS-1$ // default root 363 null, // xmlns 364 null, // default attributes 365 1 // target API level 366 ), 367 new TypeInfo("AppWidget Provider", // UI name 368 "An XML file that describes a widget provider.", // tooltip 369 ResourceFolderType.XML, // folder type 370 AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed 371 null, // default root 372 SdkConstants.NS_RESOURCES, // xmlns 373 null, // default attributes 374 3 // target API level 375 ), 376 new TypeInfo("Preference", // UI name 377 "An XML file that describes preferences.", // tooltip 378 ResourceFolderType.XML, // folder type 379 AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed 380 SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root 381 SdkConstants.NS_RESOURCES, // xmlns 382 null, // default attributes 383 1 // target API level 384 ), 385 new TypeInfo("Searchable", // UI name 386 "An XML file that describes a searchable.", // tooltip 387 ResourceFolderType.XML, // folder type 388 AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed 389 null, // default root 390 SdkConstants.NS_RESOURCES, // xmlns 391 null, // default attributes 392 1 // target API level 393 ), 394 // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in 395 // this menu since it's not often used for creating XML files. 396 }; 397 398 private NewXmlFileWizard.Values mValues; 399 private ProjectCombo mProjectButton; 400 private Text mFileNameTextField; 401 private Combo mTypeCombo; 402 private IStructuredSelection mInitialSelection; 403 private ResourceFolderType mInitialFolderType; 404 private boolean mInternalTypeUpdate; 405 private TargetChangeListener mSdkTargetChangeListener; 406 private Table mRootTable; 407 private TableViewer mRootTableViewer; 408 409 // --- UI creation --- 410 411 /** 412 * Constructs a new {@link NewXmlFileCreationPage}. 413 * <p/> 414 * Called by {@link NewXmlFileWizard#createMainPage}. 415 */ 416 protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { 417 super(pageName); 418 mValues = values; 419 setPageComplete(false); 420 } 421 422 public void setInitialSelection(IStructuredSelection initialSelection) { 423 mInitialSelection = initialSelection; 424 } 425 426 public void setInitialFolderType(ResourceFolderType initialType) { 427 mInitialFolderType = initialType; 428 } 429 430 /** 431 * Called by the parent Wizard to create the UI for this Wizard Page. 432 * 433 * {@inheritDoc} 434 * 435 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) 436 */ 437 @Override 438 @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused 439 public void createControl(Composite parent) { 440 // This UI is maintained with WindowBuilder. 441 442 Composite composite = new Composite(parent, SWT.NULL); 443 composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/)); 444 composite.setLayoutData(new GridData(GridData.FILL_BOTH)); 445 446 // label before type radios 447 Label typeLabel = new Label(composite, SWT.NONE); 448 typeLabel.setText("Resource Type:"); 449 450 mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY); 451 mTypeCombo.setToolTipText("What type of resource would you like to create?"); 452 mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 453 if (mInitialFolderType != null) { 454 mTypeCombo.setEnabled(false); 455 } 456 mTypeCombo.addSelectionListener(new SelectionAdapter() { 457 @Override 458 public void widgetSelected(SelectionEvent e) { 459 TypeInfo type = getSelectedType(); 460 if (type != null) { 461 onSelectType(type); 462 } 463 } 464 }); 465 466 // separator 467 Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); 468 GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL); 469 gd2.horizontalAlignment = SWT.FILL; 470 gd2.horizontalSpan = 2; 471 separator.setLayoutData(gd2); 472 473 // Project: [button] 474 String tooltip = "The Android Project where the new resource file will be created."; 475 Label projectLabel = new Label(composite, SWT.NONE); 476 projectLabel.setText("Project:"); 477 projectLabel.setToolTipText(tooltip); 478 479 ProjectChooserHelper helper = 480 new ProjectChooserHelper(getShell(), null /* filter */); 481 482 mProjectButton = new ProjectCombo(helper, composite, mValues.project); 483 mProjectButton.setToolTipText(tooltip); 484 mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 485 mProjectButton.addSelectionListener(new SelectionAdapter() { 486 @Override 487 public void widgetSelected(SelectionEvent e) { 488 IProject project = mProjectButton.getSelectedProject(); 489 if (project != mValues.project) { 490 changeProject(project); 491 } 492 }; 493 }); 494 495 // Filename: [text] 496 Label fileLabel = new Label(composite, SWT.NONE); 497 fileLabel.setText("File:"); 498 fileLabel.setToolTipText("The name of the resource file to create."); 499 500 mFileNameTextField = new Text(composite, SWT.BORDER); 501 mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 502 mFileNameTextField.setToolTipText(tooltip); 503 mFileNameTextField.addModifyListener(new ModifyListener() { 504 @Override 505 public void modifyText(ModifyEvent e) { 506 mValues.name = mFileNameTextField.getText(); 507 validatePage(); 508 } 509 }); 510 511 // separator 512 Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); 513 GridData gd = new GridData(GridData.GRAB_HORIZONTAL); 514 gd.horizontalAlignment = SWT.FILL; 515 gd.horizontalSpan = 2; 516 rootSeparator.setLayoutData(gd); 517 518 // Root Element: 519 // [TableViewer] 520 Label rootLabel = new Label(composite, SWT.NONE); 521 rootLabel.setText("Root Element:"); 522 new Label(composite, SWT.NONE); 523 524 mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION); 525 mRootTable = mRootTableViewer.getTable(); 526 GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); 527 tableGridData.heightHint = 200; 528 mRootTable.setLayoutData(tableGridData); 529 530 setControl(composite); 531 532 // Update state the first time 533 setErrorMessage(null); 534 setMessage(null); 535 536 initializeFromSelection(mInitialSelection); 537 updateAvailableTypes(); 538 initializeFromFixedType(); 539 initializeRootValues(); 540 installTargetChangeListener(); 541 542 initialSelectType(); 543 validatePage(); 544 } 545 546 private void initialSelectType() { 547 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 548 int typeIndex = getTypeComboIndex(mValues.type); 549 if (typeIndex == -1) { 550 typeIndex = 0; 551 } else { 552 assert mValues.type == types[typeIndex]; 553 } 554 mTypeCombo.select(typeIndex); 555 onSelectType(types[typeIndex]); 556 updateRootCombo(types[typeIndex]); 557 } 558 559 private void installTargetChangeListener() { 560 mSdkTargetChangeListener = new TargetChangeListener() { 561 @Override 562 public IProject getProject() { 563 return mValues.project; 564 } 565 566 @Override 567 public void reload() { 568 if (mValues.project != null) { 569 changeProject(mValues.project); 570 } 571 } 572 }; 573 574 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); 575 } 576 577 @Override 578 public void dispose() { 579 580 if (mSdkTargetChangeListener != null) { 581 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); 582 mSdkTargetChangeListener = null; 583 } 584 585 super.dispose(); 586 } 587 588 /** 589 * Returns the selected root element string, if any. 590 * 591 * @return The selected root element string or null. 592 */ 593 public String getRootElement() { 594 int index = mRootTable.getSelectionIndex(); 595 if (index >= 0) { 596 Object[] roots = (Object[]) mRootTableViewer.getInput(); 597 return roots[index].toString(); 598 } 599 return null; 600 } 601 602 /** 603 * Called by {@link NewXmlFileWizard} to initialize the page with the selection 604 * received by the wizard -- typically the current user workbench selection. 605 * <p/> 606 * Things we expect to find out from the selection: 607 * <ul> 608 * <li>The project name, valid if it's an android nature.</li> 609 * <li>The current folder, valid if it's a folder under /res</li> 610 * <li>An existing filename, in which case the user will be asked whether to override it.</li> 611 * </ul> 612 * <p/> 613 * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace 614 * resource path (where the resource path does not have to exist yet, such as res/anim/). 615 * 616 * @param selection The selection when the wizard was initiated. 617 */ 618 private boolean initializeFromSelection(IStructuredSelection selection) { 619 if (selection == null) { 620 return false; 621 } 622 623 // Find the best match in the element list. In case there are multiple selected elements 624 // select the one that provides the most information and assign them a score, 625 // e.g. project=1 + folder=2 + file=4. 626 IProject targetProject = null; 627 String targetWsFolderPath = null; 628 String targetFileName = null; 629 int targetScore = 0; 630 for (Object element : selection.toList()) { 631 if (element instanceof IAdaptable) { 632 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); 633 IProject project = res != null ? res.getProject() : null; 634 635 // Is this an Android project? 636 try { 637 if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) { 638 continue; 639 } 640 } catch (CoreException e) { 641 // checking the nature failed, ignore this resource 642 continue; 643 } 644 645 int score = 1; // we have a valid project at least 646 647 IPath wsFolderPath = null; 648 String fileName = null; 649 assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no 650 if (res.getType() == IResource.FOLDER) { 651 wsFolderPath = res.getProjectRelativePath(); 652 } else if (res.getType() == IResource.FILE) { 653 if (SdkUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) { 654 fileName = res.getName(); 655 } 656 wsFolderPath = res.getParent().getProjectRelativePath(); 657 } 658 659 // Disregard this folder selection if it doesn't point to /res/something 660 if (wsFolderPath != null && 661 wsFolderPath.segmentCount() > 1 && 662 SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { 663 score += 2; 664 } else { 665 wsFolderPath = null; 666 fileName = null; 667 } 668 669 score += fileName != null ? 4 : 0; 670 671 if (score > targetScore) { 672 targetScore = score; 673 targetProject = project; 674 targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; 675 targetFileName = fileName; 676 } 677 } else if (element instanceof Pair<?,?>) { 678 // Pair of Project/String 679 @SuppressWarnings("unchecked") 680 Pair<IProject,String> pair = (Pair<IProject,String>)element; 681 targetScore = 1; 682 targetProject = pair.getFirst(); 683 targetWsFolderPath = pair.getSecond(); 684 targetFileName = ""; 685 } 686 } 687 688 if (targetProject == null) { 689 // Try to figure out the project from the active editor 690 IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); 691 if (window != null) { 692 IWorkbenchPage page = window.getActivePage(); 693 if (page != null) { 694 IEditorPart activeEditor = page.getActiveEditor(); 695 if (activeEditor instanceof AndroidXmlEditor) { 696 Object input = ((AndroidXmlEditor) activeEditor).getEditorInput(); 697 if (input instanceof FileEditorInput) { 698 FileEditorInput fileInput = (FileEditorInput) input; 699 targetScore = 1; 700 IFile file = fileInput.getFile(); 701 targetProject = file.getProject(); 702 IPath path = file.getParent().getProjectRelativePath(); 703 targetWsFolderPath = path != null ? path.toString() : null; 704 } 705 } 706 } 707 } 708 } 709 710 if (targetProject == null) { 711 // If we didn't find a default project based on the selection, check how many 712 // open Android projects we can find in the current workspace. If there's only 713 // one, we'll just select it by default. 714 IJavaProject[] projects = AdtUtils.getOpenAndroidProjects(); 715 if (projects != null && projects.length == 1) { 716 targetScore = 1; 717 targetProject = projects[0].getProject(); 718 } 719 } 720 721 // Now set the UI accordingly 722 if (targetScore > 0) { 723 mValues.project = targetProject; 724 mValues.folderPath = targetWsFolderPath; 725 mProjectButton.setSelectedProject(targetProject); 726 mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ 727 728 // If the current selection context corresponds to a specific file type, 729 // select it. 730 if (targetWsFolderPath != null) { 731 int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR); 732 if (pos >= 0) { 733 targetWsFolderPath = targetWsFolderPath.substring(pos + 1); 734 } 735 String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP); 736 if (folderSegments.length > 0) { 737 mValues.configuration = FolderConfiguration.getConfig(folderSegments); 738 String folderName = folderSegments[0]; 739 selectTypeFromFolder(folderName); 740 } 741 } 742 } 743 744 return true; 745 } 746 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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