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