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 setVisible(boolean visible)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 TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)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. */ getUiName()147 String getUiName() { 148 return mUiName; 149 } 150 151 /** Returns the tooltip for the resource type. Can be null. */ getTooltip()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 */ getResFolderName()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 */ getResFolderType()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 */ getRootSeed()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 */ getDefaultRoot(IProject project)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 */ getRoots()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 */ getXmlns()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 */ getDefaultAttrs(IProject project, String root)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 */ getChild(IProject project, String root)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 */ getTargetApiLevel()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 */ NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values)419 protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { 420 super(pageName); 421 mValues = values; 422 setPageComplete(false); 423 } 424 setInitialSelection(IStructuredSelection initialSelection)425 public void setInitialSelection(IStructuredSelection initialSelection) { 426 mInitialSelection = initialSelection; 427 } 428 setInitialFolderType(ResourceFolderType initialType)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 createControl(Composite parent)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 initialSelectType()547 private void initialSelectType() { 548 TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); 549 int typeIndex = getTypeComboIndex(mValues.type); 550 if (typeIndex == -1) { 551 typeIndex = 0; 552 } else { 553 assert mValues.type == types[typeIndex]; 554 } 555 mTypeCombo.select(typeIndex); 556 onSelectType(types[typeIndex]); 557 updateRootCombo(types[typeIndex]); 558 } 559 installTargetChangeListener()560 private void installTargetChangeListener() { 561 mSdkTargetChangeListener = new TargetChangeListener() { 562 @Override 563 public IProject getProject() { 564 return mValues.project; 565 } 566 567 @Override 568 public void reload() { 569 if (mValues.project != null) { 570 changeProject(mValues.project); 571 } 572 } 573 }; 574 575 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); 576 } 577 578 @Override dispose()579 public void dispose() { 580 581 if (mSdkTargetChangeListener != null) { 582 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); 583 mSdkTargetChangeListener = null; 584 } 585 586 super.dispose(); 587 } 588 589 /** 590 * Returns the selected root element string, if any. 591 * 592 * @return The selected root element string or null. 593 */ getRootElement()594 public String getRootElement() { 595 int index = mRootTable.getSelectionIndex(); 596 if (index >= 0) { 597 Object[] roots = (Object[]) mRootTableViewer.getInput(); 598 return roots[index].toString(); 599 } 600 return null; 601 } 602 603 /** 604 * Called by {@link NewXmlFileWizard} to initialize the page with the selection 605 * received by the wizard -- typically the current user workbench selection. 606 * <p/> 607 * Things we expect to find out from the selection: 608 * <ul> 609 * <li>The project name, valid if it's an android nature.</li> 610 * <li>The current folder, valid if it's a folder under /res</li> 611 * <li>An existing filename, in which case the user will be asked whether to override it.</li> 612 * </ul> 613 * <p/> 614 * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace 615 * resource path (where the resource path does not have to exist yet, such as res/anim/). 616 * 617 * @param selection The selection when the wizard was initiated. 618 */ initializeFromSelection(IStructuredSelection selection)619 private boolean initializeFromSelection(IStructuredSelection selection) { 620 if (selection == null) { 621 return false; 622 } 623 624 // Find the best match in the element list. In case there are multiple selected elements 625 // select the one that provides the most information and assign them a score, 626 // e.g. project=1 + folder=2 + file=4. 627 IProject targetProject = null; 628 String targetWsFolderPath = null; 629 String targetFileName = null; 630 int targetScore = 0; 631 for (Object element : selection.toList()) { 632 if (element instanceof IAdaptable) { 633 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); 634 IProject project = res != null ? res.getProject() : null; 635 636 // Is this an Android project? 637 try { 638 if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) { 639 continue; 640 } 641 } catch (CoreException e) { 642 // checking the nature failed, ignore this resource 643 continue; 644 } 645 646 int score = 1; // we have a valid project at least 647 648 IPath wsFolderPath = null; 649 String fileName = null; 650 assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no 651 if (res.getType() == IResource.FOLDER) { 652 wsFolderPath = res.getProjectRelativePath(); 653 } else if (res.getType() == IResource.FILE) { 654 if (AdtUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) { 655 fileName = res.getName(); 656 } 657 wsFolderPath = res.getParent().getProjectRelativePath(); 658 } 659 660 // Disregard this folder selection if it doesn't point to /res/something 661 if (wsFolderPath != null && 662 wsFolderPath.segmentCount() > 1 && 663 SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { 664 score += 2; 665 } else { 666 wsFolderPath = null; 667 fileName = null; 668 } 669 670 score += fileName != null ? 4 : 0; 671 672 if (score > targetScore) { 673 targetScore = score; 674 targetProject = project; 675 targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; 676 targetFileName = fileName; 677 } 678 } else if (element instanceof Pair<?,?>) { 679 // Pair of Project/String 680 @SuppressWarnings("unchecked") 681 Pair<IProject,String> pair = (Pair<IProject,String>)element; 682 targetScore = 1; 683 targetProject = pair.getFirst(); 684 targetWsFolderPath = pair.getSecond(); 685 targetFileName = ""; 686 } 687 } 688 689 if (targetProject == null) { 690 // Try to figure out the project from the active editor 691 IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); 692 if (window != null) { 693 IWorkbenchPage page = window.getActivePage(); 694 if (page != null) { 695 IEditorPart activeEditor = page.getActiveEditor(); 696 if (activeEditor instanceof AndroidXmlEditor) { 697 Object input = ((AndroidXmlEditor) activeEditor).getEditorInput(); 698 if (input instanceof FileEditorInput) { 699 FileEditorInput fileInput = (FileEditorInput) input; 700 targetScore = 1; 701 IFile file = fileInput.getFile(); 702 targetProject = file.getProject(); 703 IPath path = file.getParent().getProjectRelativePath(); 704 targetWsFolderPath = path != null ? path.toString() : null; 705 } 706 } 707 } 708 } 709 } 710 711 if (targetProject == null) { 712 // If we didn't find a default project based on the selection, check how many 713 // open Android projects we can find in the current workspace. If there's only 714 // one, we'll just select it by default. 715 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 initializeFromFixedType()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 */ selectTypeFromFolder(String folderName)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 */ initializeRootValues()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 */ initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited)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 */ changeProject(IProject newProject)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 onSelectType(TypeInfo type)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 */ setSelectedType(TypeInfo type)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 */ getSelectedType()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 */ getTypeComboIndex(TypeInfo type)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 */ updateFolderPath(TypeInfo 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 */ updateRootCombo(TypeInfo type)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