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 com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.AndroidConstants; 22 import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; 23 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 24 import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; 25 import com.android.ide.eclipse.adt.internal.editors.menu.descriptors.MenuDescriptors; 26 import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors; 27 import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; 28 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration; 29 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier; 30 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType; 31 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 32 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 33 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; 34 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector; 35 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState; 36 import com.android.sdklib.IAndroidTarget; 37 import com.android.sdklib.SdkConstants; 38 39 import org.eclipse.core.resources.IFile; 40 import org.eclipse.core.resources.IProject; 41 import org.eclipse.core.resources.IResource; 42 import org.eclipse.core.runtime.CoreException; 43 import org.eclipse.core.runtime.IAdaptable; 44 import org.eclipse.core.runtime.IPath; 45 import org.eclipse.core.runtime.IStatus; 46 import org.eclipse.core.runtime.Path; 47 import org.eclipse.jdt.core.IJavaProject; 48 import org.eclipse.jface.viewers.IStructuredSelection; 49 import org.eclipse.jface.wizard.WizardPage; 50 import org.eclipse.swt.SWT; 51 import org.eclipse.swt.events.ModifyEvent; 52 import org.eclipse.swt.events.ModifyListener; 53 import org.eclipse.swt.events.SelectionAdapter; 54 import org.eclipse.swt.events.SelectionEvent; 55 import org.eclipse.swt.events.SelectionListener; 56 import org.eclipse.swt.layout.GridData; 57 import org.eclipse.swt.layout.GridLayout; 58 import org.eclipse.swt.widgets.Button; 59 import org.eclipse.swt.widgets.Combo; 60 import org.eclipse.swt.widgets.Composite; 61 import org.eclipse.swt.widgets.Label; 62 import org.eclipse.swt.widgets.Text; 63 64 import java.util.ArrayList; 65 import java.util.Collections; 66 import java.util.HashSet; 67 68 /** 69 * This is the single page of the {@link NewXmlFileWizard} which provides the ability to create 70 * skeleton XML resources files for Android projects. 71 * <p/> 72 * This page is used to select the project, the resource folder, resource type and file name. 73 */ 74 class NewXmlFileCreationPage extends WizardPage { 75 76 /** 77 * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) 78 */ 79 static class TypeInfo { 80 private final String mUiName; 81 private final ResourceFolderType mResFolderType; 82 private final String mTooltip; 83 private final Object mRootSeed; 84 private Button mWidget; 85 private ArrayList<String> mRoots = new ArrayList<String>(); 86 private final String mXmlns; 87 private final String mDefaultAttrs; 88 private final String mDefaultRoot; 89 private final int mTargetApiLevel; 90 TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel)91 public TypeInfo(String uiName, 92 String tooltip, 93 ResourceFolderType resFolderType, 94 Object rootSeed, 95 String defaultRoot, 96 String xmlns, 97 String defaultAttrs, 98 int targetApiLevel) { 99 mUiName = uiName; 100 mResFolderType = resFolderType; 101 mTooltip = tooltip; 102 mRootSeed = rootSeed; 103 mDefaultRoot = defaultRoot; 104 mXmlns = xmlns; 105 mDefaultAttrs = defaultAttrs; 106 mTargetApiLevel = targetApiLevel; 107 } 108 109 /** Returns the UI name for the resource type. Unique. Never null. */ getUiName()110 String getUiName() { 111 return mUiName; 112 } 113 114 /** Returns the tooltip for the resource type. Can be null. */ getTooltip()115 String getTooltip() { 116 return mTooltip; 117 } 118 119 /** 120 * Returns the name of the {@link ResourceFolderType}. 121 * Never null but not necessarily unique, 122 * e.g. two types use {@link ResourceFolderType#XML}. 123 */ getResFolderName()124 String getResFolderName() { 125 return mResFolderType.getName(); 126 } 127 128 /** 129 * Returns the matching {@link ResourceFolderType}. 130 * Never null but not necessarily unique, 131 * e.g. two types use {@link ResourceFolderType#XML}. 132 */ getResFolderType()133 ResourceFolderType getResFolderType() { 134 return mResFolderType; 135 } 136 137 /** Sets the radio button associate with the resource type. Can be null. */ setWidget(Button widget)138 void setWidget(Button widget) { 139 mWidget = widget; 140 } 141 142 /** Returns the radio button associate with the resource type. Can be null. */ getWidget()143 Button getWidget() { 144 return mWidget; 145 } 146 147 /** 148 * Returns the seed used to fill the root element values. 149 * The seed might be either a String, a String array, an {@link ElementDescriptor}, 150 * a {@link DocumentDescriptor} or null. 151 */ getRootSeed()152 Object getRootSeed() { 153 return mRootSeed; 154 } 155 156 /** Returns the default root element that should be selected by default. Can be null. */ getDefaultRoot()157 String getDefaultRoot() { 158 return mDefaultRoot; 159 } 160 161 /** 162 * Returns the list of all possible root elements for the resource type. 163 * This can be an empty ArrayList but not null. 164 * <p/> 165 * TODO: the root list SHOULD depend on the currently selected project, to include 166 * custom classes. 167 */ getRoots()168 ArrayList<String> getRoots() { 169 return mRoots; 170 } 171 172 /** 173 * If the generated resource XML file requires an "android" XMLNS, this should be set 174 * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. 175 */ getXmlns()176 String getXmlns() { 177 return mXmlns; 178 } 179 180 /** 181 * When not null, this represent extra attributes that must be specified in the 182 * root element of the generated XML file. When null, no extra attributes are inserted. 183 */ getDefaultAttrs()184 String getDefaultAttrs() { 185 return mDefaultAttrs; 186 } 187 188 /** 189 * The minimum API level required by the current SDK target to support this feature. 190 */ getTargetApiLevel()191 public int getTargetApiLevel() { 192 return mTargetApiLevel; 193 } 194 } 195 196 /** 197 * TypeInfo, information for each "type" of file that can be created. 198 */ 199 private static final TypeInfo[] sTypes = { 200 new TypeInfo( 201 "Layout", // UI name 202 "An XML file that describes a screen layout.", // tooltip 203 ResourceFolderType.LAYOUT, // folder type 204 AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed 205 "LinearLayout", // default root 206 SdkConstants.NS_RESOURCES, // xmlns 207 "android:layout_width=\"wrap_content\"\n" + // default attributes 208 "android:layout_height=\"wrap_content\"", 209 1 // target API level 210 ), 211 new TypeInfo("Values", // UI name 212 "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip 213 ResourceFolderType.VALUES, // folder type 214 ResourcesDescriptors.ROOT_ELEMENT, // root seed 215 null, // default root 216 null, // xmlns 217 null, // default attributes 218 1 // target API level 219 ), 220 new TypeInfo("Menu", // UI name 221 "An XML file that describes an menu.", // tooltip 222 ResourceFolderType.MENU, // folder type 223 MenuDescriptors.MENU_ROOT_ELEMENT, // root seed 224 null, // default root 225 SdkConstants.NS_RESOURCES, // xmlns 226 null, // default attributes 227 1 // target API level 228 ), 229 new TypeInfo("AppWidget Provider", // UI name 230 "An XML file that describes a widget provider.", // tooltip 231 ResourceFolderType.XML, // folder type 232 AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed 233 null, // default root 234 SdkConstants.NS_RESOURCES, // xmlns 235 null, // default attributes 236 3 // target API level 237 ), 238 new TypeInfo("Preference", // UI name 239 "An XML file that describes preferences.", // tooltip 240 ResourceFolderType.XML, // folder type 241 AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed 242 SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root 243 SdkConstants.NS_RESOURCES, // xmlns 244 null, // default attributes 245 1 // target API level 246 ), 247 new TypeInfo("Searchable", // UI name 248 "An XML file that describes a searchable.", // tooltip 249 ResourceFolderType.XML, // folder type 250 AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed 251 null, // default root 252 SdkConstants.NS_RESOURCES, // xmlns 253 null, // default attributes 254 1 // target API level 255 ), 256 new TypeInfo("Animation", // UI name 257 "An XML file that describes an animation.", // tooltip 258 ResourceFolderType.ANIM, // folder type 259 // TODO reuse constants if we ever make an editor with descriptors for animations 260 new String[] { // root seed 261 "set", //$NON-NLS-1$ 262 "alpha", //$NON-NLS-1$ 263 "scale", //$NON-NLS-1$ 264 "translate", //$NON-NLS-1$ 265 "rotate" //$NON-NLS-1$ 266 }, 267 "set", //$NON-NLS-1$ // default root 268 null, // xmlns 269 null, // default attributes 270 1 // target API level 271 ), 272 }; 273 274 /** Number of columns in the grid layout */ 275 final static int NUM_COL = 4; 276 277 /** Absolute destination folder root, e.g. "/res/" */ 278 private static final String RES_FOLDER_ABS = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP; 279 /** Relative destination folder root, e.g. "res/" */ 280 private static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; 281 282 private IProject mProject; 283 private Text mProjectTextField; 284 private Button mProjectBrowseButton; 285 private Text mFileNameTextField; 286 private Text mWsFolderPathTextField; 287 private Combo mRootElementCombo; 288 private IStructuredSelection mInitialSelection; 289 private ConfigurationSelector mConfigSelector; 290 private FolderConfiguration mTempConfig = new FolderConfiguration(); 291 private boolean mInternalWsFolderPathUpdate; 292 private boolean mInternalTypeUpdate; 293 private boolean mInternalConfigSelectorUpdate; 294 private ProjectChooserHelper mProjectChooserHelper; 295 private TargetChangeListener mSdkTargetChangeListener; 296 297 private TypeInfo mCurrentTypeInfo; 298 299 // --- UI creation --- 300 301 /** 302 * Constructs a new {@link NewXmlFileCreationPage}. 303 * <p/> 304 * Called by {@link NewXmlFileWizard#createMainPage()}. 305 */ NewXmlFileCreationPage(String pageName)306 protected NewXmlFileCreationPage(String pageName) { 307 super(pageName); 308 setPageComplete(false); 309 } 310 setInitialSelection(IStructuredSelection initialSelection)311 public void setInitialSelection(IStructuredSelection initialSelection) { 312 mInitialSelection = initialSelection; 313 } 314 315 /** 316 * Called by the parent Wizard to create the UI for this Wizard Page. 317 * 318 * {@inheritDoc} 319 * 320 * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) 321 */ createControl(Composite parent)322 public void createControl(Composite parent) { 323 Composite composite = new Composite(parent, SWT.NULL); 324 composite.setFont(parent.getFont()); 325 326 initializeDialogUnits(parent); 327 328 composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/)); 329 composite.setLayoutData(new GridData(GridData.FILL_BOTH)); 330 331 createProjectGroup(composite); 332 createTypeGroup(composite); 333 createRootGroup(composite); 334 335 // Show description the first time 336 setErrorMessage(null); 337 setMessage(null); 338 setControl(composite); 339 340 // Update state the first time 341 initializeFromSelection(mInitialSelection); 342 initializeRootValues(); 343 enableTypesBasedOnApi(); 344 if (mCurrentTypeInfo != null) { 345 updateRootCombo(mCurrentTypeInfo); 346 } 347 installTargetChangeListener(); 348 validatePage(); 349 } 350 installTargetChangeListener()351 private void installTargetChangeListener() { 352 mSdkTargetChangeListener = new TargetChangeListener() { 353 @Override 354 public IProject getProject() { 355 return mProject; 356 } 357 358 @Override 359 public void reload() { 360 if (mProject != null) { 361 changeProject(mProject); 362 } 363 } 364 }; 365 366 AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); 367 } 368 369 @Override dispose()370 public void dispose() { 371 372 if (mSdkTargetChangeListener != null) { 373 AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); 374 mSdkTargetChangeListener = null; 375 } 376 377 super.dispose(); 378 } 379 380 /** 381 * Returns the target project or null. 382 */ getProject()383 public IProject getProject() { 384 return mProject; 385 } 386 387 /** 388 * Returns the destination filename or an empty string. 389 */ getFileName()390 public String getFileName() { 391 return mFileNameTextField == null ? "" : mFileNameTextField.getText(); //$NON-NLS-1$ 392 } 393 394 /** 395 * Returns the destination folder path relative to the project or an empty string. 396 */ getWsFolderPath()397 public String getWsFolderPath() { 398 return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$ 399 } 400 401 402 /** 403 * Returns an {@link IFile} on the destination file. 404 * <p/> 405 * Uses {@link #getProject()}, {@link #getWsFolderPath()} and {@link #getFileName()}. 406 * <p/> 407 * Returns null if the project, filename or folder are invalid and the destination file 408 * cannot be determined. 409 * <p/> 410 * The {@link IFile} is a resource. There might or might not be an actual real file. 411 */ getDestinationFile()412 public IFile getDestinationFile() { 413 IProject project = getProject(); 414 String wsFolderPath = getWsFolderPath(); 415 String fileName = getFileName(); 416 if (project != null && wsFolderPath.length() > 0 && fileName.length() > 0) { 417 IPath dest = new Path(wsFolderPath).append(fileName); 418 IFile file = project.getFile(dest); 419 return file; 420 } 421 return null; 422 } 423 424 /** 425 * Returns the {@link TypeInfo} for the currently selected type radio button. 426 * Returns null if no radio button is selected. 427 * 428 * @return A {@link TypeInfo} or null. 429 */ getSelectedType()430 public TypeInfo getSelectedType() { 431 TypeInfo type = null; 432 for (TypeInfo ti : sTypes) { 433 if (ti.getWidget().getSelection()) { 434 type = ti; 435 break; 436 } 437 } 438 return type; 439 } 440 441 /** 442 * Returns the selected root element string, if any. 443 * 444 * @return The selected root element string or null. 445 */ getRootElement()446 public String getRootElement() { 447 int index = mRootElementCombo.getSelectionIndex(); 448 if (index >= 0) { 449 return mRootElementCombo.getItem(index); 450 } 451 return null; 452 } 453 454 // --- UI creation --- 455 456 /** 457 * Helper method to create a new GridData with an horizontal span. 458 * 459 * @param horizSpan The number of cells for the horizontal span. 460 * @return A new GridData with the horizontal span. 461 */ newGridData(int horizSpan)462 private GridData newGridData(int horizSpan) { 463 GridData gd = new GridData(); 464 gd.horizontalSpan = horizSpan; 465 return gd; 466 } 467 468 /** 469 * Helper method to create a new GridData with an horizontal span and a style. 470 * 471 * @param horizSpan The number of cells for the horizontal span. 472 * @param style The style, e.g. {@link GridData#FILL_HORIZONTAL} 473 * @return A new GridData with the horizontal span and the style. 474 */ newGridData(int horizSpan, int style)475 private GridData newGridData(int horizSpan, int style) { 476 GridData gd = new GridData(style); 477 gd.horizontalSpan = horizSpan; 478 return gd; 479 } 480 481 /** 482 * Helper method that creates an empty cell in the parent composite. 483 * 484 * @param parent The parent composite. 485 */ emptyCell(Composite parent)486 private void emptyCell(Composite parent) { 487 new Label(parent, SWT.NONE); 488 } 489 490 /** 491 * Pads the parent with empty cells to match the number of columns of the parent grid. 492 * 493 * @param parent A grid layout with NUM_COL columns 494 * @param col The current number of columns used. 495 * @return 0, the new number of columns used, for convenience. 496 */ padWithEmptyCells(Composite parent, int col)497 private int padWithEmptyCells(Composite parent, int col) { 498 for (; col < NUM_COL; ++col) { 499 emptyCell(parent); 500 } 501 col = 0; 502 return col; 503 } 504 505 /** 506 * Creates the project & filename fields. 507 * <p/> 508 * The parent must be a GridLayout with NUM_COL colums. 509 */ createProjectGroup(Composite parent)510 private void createProjectGroup(Composite parent) { 511 int col = 0; 512 513 // project name 514 String tooltip = "The Android Project where the new resource file will be created."; 515 Label label = new Label(parent, SWT.NONE); 516 label.setText("Project"); 517 label.setToolTipText(tooltip); 518 ++col; 519 520 mProjectTextField = new Text(parent, SWT.BORDER); 521 mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 522 mProjectTextField.setToolTipText(tooltip); 523 mProjectTextField.addModifyListener(new ModifyListener() { 524 public void modifyText(ModifyEvent e) { 525 onProjectFieldUpdated(); 526 } 527 }); 528 ++col; 529 530 mProjectBrowseButton = new Button(parent, SWT.NONE); 531 mProjectBrowseButton.setText("Browse..."); 532 mProjectBrowseButton.setToolTipText("Allows you to select the Android project to modify."); 533 mProjectBrowseButton.addSelectionListener(new SelectionAdapter() { 534 @Override 535 public void widgetSelected(SelectionEvent e) { 536 onProjectBrowse(); 537 } 538 }); 539 mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/); 540 ++col; 541 542 col = padWithEmptyCells(parent, col); 543 544 // file name 545 tooltip = "The name of the resource file to create."; 546 label = new Label(parent, SWT.NONE); 547 label.setText("File"); 548 label.setToolTipText(tooltip); 549 ++col; 550 551 mFileNameTextField = new Text(parent, SWT.BORDER); 552 mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 553 mFileNameTextField.setToolTipText(tooltip); 554 mFileNameTextField.addModifyListener(new ModifyListener() { 555 public void modifyText(ModifyEvent e) { 556 validatePage(); 557 } 558 }); 559 ++col; 560 561 padWithEmptyCells(parent, col); 562 } 563 564 /** 565 * Creates the type field, {@link ConfigurationSelector} and the folder field. 566 * <p/> 567 * The parent must be a GridLayout with NUM_COL colums. 568 */ createTypeGroup(Composite parent)569 private void createTypeGroup(Composite parent) { 570 // separator 571 Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); 572 label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); 573 574 // label before type radios 575 label = new Label(parent, SWT.NONE); 576 label.setText("What type of resource would you like to create?"); 577 label.setLayoutData(newGridData(NUM_COL)); 578 579 // display the types on three columns of radio buttons. 580 emptyCell(parent); 581 Composite grid = new Composite(parent, SWT.NONE); 582 padWithEmptyCells(parent, 2); 583 584 grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/)); 585 586 SelectionListener radioListener = new SelectionAdapter() { 587 @Override 588 public void widgetSelected(SelectionEvent e) { 589 // single-click. Only do something if activated. 590 if (e.getSource() instanceof Button) { 591 onRadioTypeUpdated((Button) e.getSource()); 592 } 593 } 594 }; 595 596 int n = sTypes.length; 597 int num_lines = (n + NUM_COL/2) / NUM_COL; 598 for (int line = 0, k = 0; line < num_lines; line++) { 599 for (int i = 0; i < NUM_COL; i++, k++) { 600 if (k < n) { 601 TypeInfo type = sTypes[k]; 602 Button radio = new Button(grid, SWT.RADIO); 603 type.setWidget(radio); 604 radio.setSelection(false); 605 radio.setText(type.getUiName()); 606 radio.setToolTipText(type.getTooltip()); 607 radio.addSelectionListener(radioListener); 608 } else { 609 emptyCell(grid); 610 } 611 } 612 } 613 614 // label before configuration selector 615 label = new Label(parent, SWT.NONE); 616 label.setText("What type of resource configuration would you like?"); 617 label.setLayoutData(newGridData(NUM_COL)); 618 619 // configuration selector 620 emptyCell(parent); 621 mConfigSelector = new ConfigurationSelector(parent, false /* deviceMode*/); 622 GridData gd = newGridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); 623 gd.widthHint = ConfigurationSelector.WIDTH_HINT; 624 gd.heightHint = ConfigurationSelector.HEIGHT_HINT; 625 mConfigSelector.setLayoutData(gd); 626 mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated()); 627 emptyCell(parent); 628 629 // folder name 630 String tooltip = "The folder where the file will be generated, relative to the project."; 631 label = new Label(parent, SWT.NONE); 632 label.setText("Folder"); 633 label.setToolTipText(tooltip); 634 635 mWsFolderPathTextField = new Text(parent, SWT.BORDER); 636 mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 637 mWsFolderPathTextField.setToolTipText(tooltip); 638 mWsFolderPathTextField.addModifyListener(new ModifyListener() { 639 public void modifyText(ModifyEvent e) { 640 onWsFolderPathUpdated(); 641 } 642 }); 643 } 644 645 /** 646 * Creates the root element combo. 647 * <p/> 648 * The parent must be a GridLayout with NUM_COL colums. 649 */ createRootGroup(Composite parent)650 private void createRootGroup(Composite parent) { 651 // separator 652 Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); 653 label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); 654 655 // label before the root combo 656 String tooltip = "The root element to create in the XML file."; 657 label = new Label(parent, SWT.NONE); 658 label.setText("Select the root element for the XML file:"); 659 label.setLayoutData(newGridData(NUM_COL)); 660 label.setToolTipText(tooltip); 661 662 // root combo 663 emptyCell(parent); 664 665 mRootElementCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); 666 mRootElementCombo.setEnabled(false); 667 mRootElementCombo.select(0); 668 mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 669 mRootElementCombo.setToolTipText(tooltip); 670 671 padWithEmptyCells(parent, 2); 672 } 673 674 /** 675 * Called by {@link NewXmlFileWizard} to initialize the page with the selection 676 * received by the wizard -- typically the current user workbench selection. 677 * <p/> 678 * Things we expect to find out from the selection: 679 * <ul> 680 * <li>The project name, valid if it's an android nature.</li> 681 * <li>The current folder, valid if it's a folder under /res</li> 682 * <li>An existing filename, in which case the user will be asked whether to override it.</li> 683 * <ul> 684 * 685 * @param selection The selection when the wizard was initiated. 686 */ initializeFromSelection(IStructuredSelection selection)687 private void initializeFromSelection(IStructuredSelection selection) { 688 if (selection == null) { 689 return; 690 } 691 692 // Find the best match in the element list. In case there are multiple selected elements 693 // select the one that provides the most information and assign them a score, 694 // e.g. project=1 + folder=2 + file=4. 695 IProject targetProject = null; 696 String targetWsFolderPath = null; 697 String targetFileName = null; 698 int targetScore = 0; 699 for (Object element : selection.toList()) { 700 if (element instanceof IAdaptable) { 701 IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); 702 IProject project = res != null ? res.getProject() : null; 703 704 // Is this an Android project? 705 try { 706 if (project == null || !project.hasNature(AndroidConstants.NATURE_DEFAULT)) { 707 continue; 708 } 709 } catch (CoreException e) { 710 // checking the nature failed, ignore this resource 711 continue; 712 } 713 714 int score = 1; // we have a valid project at least 715 716 IPath wsFolderPath = null; 717 String fileName = null; 718 if (res.getType() == IResource.FOLDER) { 719 wsFolderPath = res.getProjectRelativePath(); 720 } else if (res.getType() == IResource.FILE) { 721 fileName = res.getName(); 722 wsFolderPath = res.getParent().getProjectRelativePath(); 723 } 724 725 // Disregard this folder selection if it doesn't point to /res/something 726 if (wsFolderPath != null && 727 wsFolderPath.segmentCount() > 1 && 728 SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { 729 score += 2; 730 } else { 731 wsFolderPath = null; 732 fileName = null; 733 } 734 735 score += fileName != null ? 4 : 0; 736 737 if (score > targetScore) { 738 targetScore = score; 739 targetProject = project; 740 targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; 741 targetFileName = fileName; 742 } 743 } 744 } 745 746 // Now set the UI accordingly 747 if (targetScore > 0) { 748 mProject = targetProject; 749 mProjectTextField.setText(targetProject != null ? targetProject.getName() : ""); //$NON-NLS-1$ 750 mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ 751 mWsFolderPathTextField.setText(targetWsFolderPath != null ? targetWsFolderPath : ""); //$NON-NLS-1$ 752 } 753 } 754 755 /** 756 * Initialize the root values of the type infos based on the current framework values. 757 */ initializeRootValues()758 private void initializeRootValues() { 759 for (TypeInfo type : sTypes) { 760 // Clear all the roots for this type 761 ArrayList<String> roots = type.getRoots(); 762 if (roots.size() > 0) { 763 roots.clear(); 764 } 765 766 // depending of the type of the seed, initialize the root in different ways 767 Object rootSeed = type.getRootSeed(); 768 769 if (rootSeed instanceof String) { 770 // The seed is a single string, Add it as-is. 771 roots.add((String) rootSeed); 772 } else if (rootSeed instanceof String[]) { 773 // The seed is an array of strings. Add them as-is. 774 for (String value : (String[]) rootSeed) { 775 roots.add(value); 776 } 777 } else if (rootSeed instanceof Integer && mProject != null) { 778 // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_* 779 // In this case add all the children element descriptors defined, recursively, 780 // and avoid infinite recursion by keeping track of what has already been added. 781 782 // Note: if project is null, the root list will be empty since it has been 783 // cleared above. 784 785 // get the AndroidTargetData from the project 786 IAndroidTarget target = null; 787 AndroidTargetData data = null; 788 789 target = Sdk.getCurrent().getTarget(mProject); 790 if (target == null) { 791 // A project should have a target. The target can be missing if the project 792 // is an old project for which a target hasn't been affected or if the 793 // target no longer exists in this SDK. Simply log the error and dismiss. 794 795 AdtPlugin.log(IStatus.INFO, 796 "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$ 797 mProject.getName()); 798 continue; 799 } else { 800 data = Sdk.getCurrent().getTargetData(target); 801 802 if (data == null) { 803 // We should have both a target and its data. 804 // However if the wizard is invoked whilst the platform is still being 805 // loaded we can end up in a weird case where we have a target but it 806 // doesn't have any data yet. 807 // Lets log a warning and silently ignore this root. 808 809 AdtPlugin.log(IStatus.INFO, 810 "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$ 811 target.getName(), mProject.getName()); 812 continue; 813 } 814 } 815 816 IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); 817 ElementDescriptor descriptor = provider.getDescriptor(); 818 if (descriptor != null) { 819 HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>(); 820 initRootElementDescriptor(roots, descriptor, visited); 821 } 822 823 // Sort alphabetically. 824 Collections.sort(roots); 825 } 826 } 827 } 828 829 /** 830 * Helper method to recursively insert all XML names for the given {@link ElementDescriptor} 831 * into the roots array list. Keeps track of visited nodes to avoid infinite recursion. 832 * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic 833 * and not a valid root element. 834 */ initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited)835 private void initRootElementDescriptor(ArrayList<String> roots, 836 ElementDescriptor desc, HashSet<ElementDescriptor> visited) { 837 if (!(desc instanceof DocumentDescriptor)) { 838 String xmlName = desc.getXmlName(); 839 if (xmlName != null && xmlName.length() > 0) { 840 roots.add(xmlName); 841 } 842 } 843 844 visited.add(desc); 845 846 for (ElementDescriptor child : desc.getChildren()) { 847 if (!visited.contains(child)) { 848 initRootElementDescriptor(roots, child, visited); 849 } 850 } 851 } 852 853 /** 854 * Callback called when the user edits the project text field. 855 */ onProjectFieldUpdated()856 private void onProjectFieldUpdated() { 857 String project = mProjectTextField.getText(); 858 859 // Is this a valid project? 860 IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/); 861 IProject found = null; 862 for (IJavaProject p : projects) { 863 if (p.getProject().getName().equals(project)) { 864 found = p.getProject(); 865 break; 866 } 867 } 868 869 if (found != mProject) { 870 changeProject(found); 871 } 872 } 873 874 /** 875 * Callback called when the user uses the "Browse Projects" button. 876 */ onProjectBrowse()877 private void onProjectBrowse() { 878 IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText(), 879 "Please select the target project"); 880 if (p != null) { 881 changeProject(p.getProject()); 882 mProjectTextField.setText(mProject.getName()); 883 } 884 } 885 886 /** 887 * Changes mProject to the given new project and update the UI accordingly. 888 * <p/> 889 * Note that this does not check if the new project is the same as the current one 890 * on purpose, which allows a project to be updated when its target has changed or 891 * when targets are loaded in the background. 892 */ changeProject(IProject newProject)893 private void changeProject(IProject newProject) { 894 mProject = newProject; 895 896 // enable types based on new API level 897 enableTypesBasedOnApi(); 898 899 // update the folder name based on API level 900 resetFolderPath(false /*validate*/); 901 902 // update the Type with the new descriptors. 903 initializeRootValues(); 904 905 // update the combo 906 updateRootCombo(getSelectedType()); 907 908 validatePage(); 909 } 910 911 /** 912 * Callback called when the Folder text field is changed, either programmatically 913 * or by the user. 914 */ onWsFolderPathUpdated()915 private void onWsFolderPathUpdated() { 916 if (mInternalWsFolderPathUpdate) { 917 return; 918 } 919 920 String wsFolderPath = mWsFolderPathTextField.getText(); 921 922 // This is a custom path, we need to sanitize it. 923 // First it should start with "/res/". Then we need to make sure there are no 924 // relative paths, things like "../" or "./" or even "//". 925 wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$ 926 wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$ 927 wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ 928 929 ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>(); 930 931 // We get "res/foo" from selections relative to the project when we want a "/res/foo" path. 932 if (wsFolderPath.startsWith(RES_FOLDER_REL)) { 933 wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length()); 934 935 mInternalWsFolderPathUpdate = true; 936 mWsFolderPathTextField.setText(wsFolderPath); 937 mInternalWsFolderPathUpdate = false; 938 } 939 940 if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { 941 wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); 942 943 int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR); 944 if (pos >= 0) { 945 wsFolderPath = wsFolderPath.substring(0, pos); 946 } 947 948 String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP); 949 950 if (folderSegments.length > 0) { 951 String folderName = folderSegments[0]; 952 953 // update config selector 954 mInternalConfigSelectorUpdate = true; 955 mConfigSelector.setConfiguration(folderSegments); 956 mInternalConfigSelectorUpdate = false; 957 958 boolean selected = false; 959 for (TypeInfo type : sTypes) { 960 if (type.getResFolderName().equals(folderName)) { 961 matches.add(type); 962 selected |= type.getWidget().getSelection(); 963 } 964 } 965 966 if (matches.size() == 1) { 967 // If there's only one match, select it if it's not already selected 968 if (!selected) { 969 selectType(matches.get(0)); 970 } 971 } else if (matches.size() > 1) { 972 // There are multiple type candidates for this folder. This can happen 973 // for /res/xml for example. Check to see if one of them is currently 974 // selected. If yes, leave the selection unchanged. If not, deselect all type. 975 if (!selected) { 976 selectType(null); 977 } 978 } else { 979 // Nothing valid was selected. 980 selectType(null); 981 } 982 } 983 } 984 985 validatePage(); 986 } 987 988 /** 989 * Callback called when one of the type radio button is changed. 990 * 991 * @param typeWidget The type radio button that changed. 992 */ onRadioTypeUpdated(Button typeWidget)993 private void onRadioTypeUpdated(Button typeWidget) { 994 // Do nothing if this is an internal modification or if the widget has been 995 // de-selected. 996 if (mInternalTypeUpdate || !typeWidget.getSelection()) { 997 return; 998 } 999 1000 // Find type info that has just been enabled. 1001 TypeInfo type = null; 1002 for (TypeInfo ti : sTypes) { 1003 if (ti.getWidget() == typeWidget) { 1004 type = ti; 1005 break; 1006 } 1007 } 1008 1009 if (type == null) { 1010 return; 1011 } 1012 1013 // update the combo 1014 1015 updateRootCombo(type); 1016 1017 // update the folder path 1018 1019 String wsFolderPath = mWsFolderPathTextField.getText(); 1020 String newPath = null; 1021 1022 mConfigSelector.getConfiguration(mTempConfig); 1023 ResourceQualifier qual = mTempConfig.getInvalidQualifier(); 1024 if (qual == null) { 1025 // The configuration is valid. Reformat the folder path using the canonical 1026 // value from the configuration. 1027 1028 newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType()); 1029 } else { 1030 // The configuration is invalid. We still update the path but this time 1031 // do it manually on the string. 1032 if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { 1033 wsFolderPath.replaceFirst( 1034 "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$ 1035 "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$ 1036 } else { 1037 newPath = RES_FOLDER_ABS + mTempConfig.getFolderName(type.getResFolderType()); 1038 } 1039 } 1040 1041 if (newPath != null && !newPath.equals(wsFolderPath)) { 1042 mInternalWsFolderPathUpdate = true; 1043 mWsFolderPathTextField.setText(newPath); 1044 mInternalWsFolderPathUpdate = false; 1045 } 1046 1047 validatePage(); 1048 } 1049 1050 /** 1051 * Helper method that fills the values of the "root element" combo box based 1052 * on the currently selected type radio button. Also disables the combo is there's 1053 * only one choice. Always select the first root element for the given type. 1054 * 1055 * @param type The currently selected {@link TypeInfo}. Cannot be null. 1056 */ updateRootCombo(TypeInfo type)1057 private void updateRootCombo(TypeInfo type) { 1058 // reset all the values in the combo 1059 mRootElementCombo.removeAll(); 1060 1061 if (type != null) { 1062 // get the list of roots. The list can be empty but not null. 1063 ArrayList<String> roots = type.getRoots(); 1064 1065 // enable the combo if there's more than one choice 1066 mRootElementCombo.setEnabled(roots != null && roots.size() > 1); 1067 1068 for (String root : roots) { 1069 mRootElementCombo.add(root); 1070 } 1071 1072 int index = 0; // default is to select the first one 1073 String defaultRoot = type.getDefaultRoot(); 1074 if (defaultRoot != null) { 1075 index = roots.indexOf(defaultRoot); 1076 } 1077 mRootElementCombo.select(index < 0 ? 0 : index); 1078 } 1079 } 1080 1081 /** 1082 * Callback called when the configuration has changed in the {@link ConfigurationSelector}. 1083 */ 1084 private class onConfigSelectorUpdated implements Runnable { 1085 public void run() { 1086 if (mInternalConfigSelectorUpdate) { 1087 return; 1088 } 1089 1090 resetFolderPath(true /*validate*/); 1091 } 1092 } 1093 1094 /** 1095 * Helper method to select on of the type radio buttons. 1096 * 1097 * @param type The TypeInfo matching the radio button to selected or null to deselect them all. 1098 */ 1099 private void selectType(TypeInfo type) { 1100 if (type == null || !type.getWidget().getSelection()) { 1101 mInternalTypeUpdate = true; 1102 mCurrentTypeInfo = type; 1103 for (TypeInfo type2 : sTypes) { 1104 type2.getWidget().setSelection(type2 == type); 1105 } 1106 updateRootCombo(type); 1107 mInternalTypeUpdate = false; 1108 } 1109 } 1110 1111 /** 1112 * Helper method to enable the type radio buttons depending on the current API level. 1113 * <p/> 1114 * A type radio button is enabled either if: 1115 * - if mProject is null, API level 1 is considered valid 1116 * - if mProject is !null, the project->target->API must be >= to the type's API level. 1117 */ 1118 private void enableTypesBasedOnApi() { 1119 1120 IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null; 1121 int currentApiLevel = 1; 1122 if (target != null) { 1123 currentApiLevel = target.getVersion().getApiLevel(); 1124 } 1125 1126 for (TypeInfo type : sTypes) { 1127 type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel); 1128 } 1129 } 1130 1131 /** 1132 * Reset the current Folder path based on the UI selection 1133 * @param validate if true, force a call to {@link #validatePage()}. 1134 */ 1135 private void resetFolderPath(boolean validate) { 1136 TypeInfo type = getSelectedType(); 1137 1138 if (type != null) { 1139 mConfigSelector.getConfiguration(mTempConfig); 1140 StringBuffer sb = new StringBuffer(RES_FOLDER_ABS); 1141 sb.append(mTempConfig.getFolderName(type.getResFolderType())); 1142 1143 mInternalWsFolderPathUpdate = true; 1144 mWsFolderPathTextField.setText(sb.toString()); 1145 mInternalWsFolderPathUpdate = false; 1146 1147 if (validate) { 1148 validatePage(); 1149 } 1150 } 1151 } 1152 1153 /** 1154 * Validates the fields, displays errors and warnings. 1155 * Enables the finish button if there are no errors. 1156 */ 1157 private void validatePage() { 1158 String error = null; 1159 String warning = null; 1160 1161 // -- validate project 1162 if (getProject() == null) { 1163 error = "Please select an Android project."; 1164 } 1165 1166 // -- validate filename 1167 if (error == null) { 1168 String fileName = getFileName(); 1169 if (fileName == null || fileName.length() == 0) { 1170 error = "A destination file name is required."; 1171 } else if (!fileName.endsWith(AndroidConstants.DOT_XML)) { 1172 error = String.format("The filename must end with %1$s.", AndroidConstants.DOT_XML); 1173 } 1174 } 1175 1176 // -- validate type 1177 if (error == null) { 1178 TypeInfo type = getSelectedType(); 1179 1180 if (type == null) { 1181 error = "One of the types must be selected (e.g. layout, values, etc.)"; 1182 } 1183 } 1184 1185 // -- validate type API level 1186 if (error == null) { 1187 IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); 1188 int currentApiLevel = 1; 1189 if (target != null) { 1190 currentApiLevel = target.getVersion().getApiLevel(); 1191 } 1192 1193 TypeInfo type = getSelectedType(); 1194 1195 if (type.getTargetApiLevel() > currentApiLevel) { 1196 error = "The API level of the selected type (e.g. AppWidget, etc.) is not " + 1197 "compatible with the API level of the project."; 1198 } 1199 } 1200 1201 // -- validate folder configuration 1202 if (error == null) { 1203 ConfigurationState state = mConfigSelector.getState(); 1204 if (state == ConfigurationState.INVALID_CONFIG) { 1205 ResourceQualifier qual = mConfigSelector.getInvalidQualifier(); 1206 if (qual != null) { 1207 error = String.format("The qualifier '%1$s' is invalid in the folder configuration.", 1208 qual.getName()); 1209 } 1210 } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) { 1211 error = "The Region qualifier requires the Language qualifier."; 1212 } 1213 } 1214 1215 // -- validate generated path 1216 if (error == null) { 1217 String wsFolderPath = getWsFolderPath(); 1218 if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) { 1219 error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS); 1220 } 1221 } 1222 1223 // -- validate destination file doesn't exist 1224 if (error == null) { 1225 IFile file = getDestinationFile(); 1226 if (file != null && file.exists()) { 1227 warning = "The destination file already exists"; 1228 } 1229 } 1230 1231 // -- update UI & enable finish if there's no error 1232 setPageComplete(error == null); 1233 if (error != null) { 1234 setMessage(error, WizardPage.ERROR); 1235 } else if (warning != null) { 1236 setMessage(warning, WizardPage.WARNING); 1237 } else { 1238 setErrorMessage(null); 1239 setMessage(null); 1240 } 1241 } 1242 1243 } 1244