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 setVisible(boolean visible)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 TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)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. */ getUiName()143 String getUiName() { 144 return mUiName; 145 } 146 147 /** Returns the tooltip for the resource type. Can be null. */ getTooltip()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 */ getResFolderName()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 */ getResFolderType()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 */ getRootSeed()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 */ getDefaultRoot(IProject project)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 */ getRoots()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 */ getXmlns()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 */ getDefaultAttrs(IProject project, String root)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 */ getChild(IProject project, String root)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 */ getTargetApiLevel()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 */ NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values)416 protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { 417 super(pageName); 418 mValues = values; 419 setPageComplete(false); 420 } 421 setInitialSelection(IStructuredSelection initialSelection)422 public void setInitialSelection(IStructuredSelection initialSelection) { 423 mInitialSelection = initialSelection; 424 } 425 setInitialFolderType(ResourceFolderType initialType)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 createControl(Composite parent)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 initialSelectType()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 installTargetChangeListener()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 dispose()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 */ getRootElement()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 */ initializeFromSelection(IStructuredSelection selection)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 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