/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; import static com.android.SdkConstants.LINEAR_LAYOUT; import static com.android.SdkConstants.RES_QUALIFIER_SEP; import static com.android.SdkConstants.SCROLL_VIEW; import static com.android.SdkConstants.VALUE_FILL_PARENT; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP_CHAR; import static com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ChooseConfigurationPage.RES_FOLDER_ABS; import com.android.SdkConstants; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.ResourceQualifier; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper; import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.ProjectCombo; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; import com.android.utils.Pair; import com.android.utils.SdkUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.FileEditorInput; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; /** * This is the first page of the {@link NewXmlFileWizard} which provides the ability to create * skeleton XML resources files for Android projects. * <p/> * This page is used to select the project, resource type and file name. */ class NewXmlFileCreationPage extends WizardPage { @Override public void setVisible(boolean visible) { super.setVisible(visible); // Ensure the initial focus is in the Name field; you usually don't need // to edit the default text field (the project name) if (visible && mFileNameTextField != null) { mFileNameTextField.setFocus(); } validatePage(); } /** * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) */ static class TypeInfo { private final String mUiName; private final ResourceFolderType mResFolderType; private final String mTooltip; private final Object mRootSeed; private ArrayList<String> mRoots = new ArrayList<String>(); private final String mXmlns; private final String mDefaultAttrs; private final String mDefaultRoot; private final int mTargetApiLevel; public TypeInfo(String uiName, String tooltip, ResourceFolderType resFolderType, Object rootSeed, String defaultRoot, String xmlns, String defaultAttrs, int targetApiLevel) { mUiName = uiName; mResFolderType = resFolderType; mTooltip = tooltip; mRootSeed = rootSeed; mDefaultRoot = defaultRoot; mXmlns = xmlns; mDefaultAttrs = defaultAttrs; mTargetApiLevel = targetApiLevel; } /** Returns the UI name for the resource type. Unique. Never null. */ String getUiName() { return mUiName; } /** Returns the tooltip for the resource type. Can be null. */ String getTooltip() { return mTooltip; } /** * Returns the name of the {@link ResourceFolderType}. * Never null but not necessarily unique, * e.g. two types use {@link ResourceFolderType#XML}. */ String getResFolderName() { return mResFolderType.getName(); } /** * Returns the matching {@link ResourceFolderType}. * Never null but not necessarily unique, * e.g. two types use {@link ResourceFolderType#XML}. */ ResourceFolderType getResFolderType() { return mResFolderType; } /** * Returns the seed used to fill the root element values. * The seed might be either a String, a String array, an {@link ElementDescriptor}, * a {@link DocumentDescriptor} or null. */ Object getRootSeed() { return mRootSeed; } /** * Returns the default root element that should be selected by default. Can be * null. * * @param project the associated project, or null if not known */ String getDefaultRoot(IProject project) { return mDefaultRoot; } /** * Returns the list of all possible root elements for the resource type. * This can be an empty ArrayList but not null. * <p/> * TODO: the root list SHOULD depend on the currently selected project, to include * custom classes. */ ArrayList<String> getRoots() { return mRoots; } /** * If the generated resource XML file requires an "android" XMLNS, this should be set * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. */ String getXmlns() { return mXmlns; } /** * When not null, this represent extra attributes that must be specified in the * root element of the generated XML file. When null, no extra attributes are inserted. * * @param project the project to get the attributes for * @param root the selected root element string, never null */ String getDefaultAttrs(IProject project, String root) { return mDefaultAttrs; } /** * When not null, represents an extra string that should be written inside * the element when constructed * * @param project the project to get the child content for * @param root the chosen root element * @return a string to be written inside the root element, or null if nothing */ String getChild(IProject project, String root) { return null; } /** * The minimum API level required by the current SDK target to support this feature. * * @return the minimum API level */ public int getTargetApiLevel() { return mTargetApiLevel; } } /** * TypeInfo, information for each "type" of file that can be created. */ private static final TypeInfo[] sTypes = { new TypeInfo( "Layout", // UI name "An XML file that describes a screen layout.", // tooltip ResourceFolderType.LAYOUT, // folder type AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed LINEAR_LAYOUT, // default root SdkConstants.NS_RESOURCES, // xmlns "", // not used, see below 1 // target API level ) { @Override String getDefaultRoot(IProject project) { // TODO: Use GridLayout by default for new SDKs // (when we've ironed out all the usability issues) //Sdk currentSdk = Sdk.getCurrent(); //if (project != null && currentSdk != null) { // IAndroidTarget target = currentSdk.getTarget(project); // // fill_parent was renamed match_parent in API level 8 // if (target != null && target.getVersion().getApiLevel() >= 13) { // return GRID_LAYOUT; // } //} return LINEAR_LAYOUT; }; // The default attributes must be determined dynamically since whether // we use match_parent or fill_parent depends on the API level of the // project @Override String getDefaultAttrs(IProject project, String root) { Sdk currentSdk = Sdk.getCurrent(); String fill = VALUE_FILL_PARENT; if (currentSdk != null) { IAndroidTarget target = currentSdk.getTarget(project); // fill_parent was renamed match_parent in API level 8 if (target != null && target.getVersion().getApiLevel() >= 8) { fill = VALUE_MATCH_PARENT; } } // Only set "vertical" orientation of LinearLayouts by default; // for GridLayouts for example we want to rely on the real default // of the layout String size = String.format( "android:layout_width=\"%1$s\"\n" //$NON-NLS-1$ + "android:layout_height=\"%2$s\"", //$NON-NLS-1$ fill, fill); if (LINEAR_LAYOUT.equals(root)) { return "android:orientation=\"vertical\"\n" + size; //$NON-NLS-1$ } else { return size; } } @Override String getChild(IProject project, String root) { // Create vertical linear layouts inside new scroll views if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { return " <LinearLayout " //$NON-NLS-1$ + getDefaultAttrs(project, root).replace('\n', ' ') + "></LinearLayout>\n"; //$NON-NLS-1$ } return null; } }, new TypeInfo("Values", // UI name "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip ResourceFolderType.VALUES, // folder type SdkConstants.TAG_RESOURCES, // root seed null, // default root null, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("Drawable", // UI name "An XML file that describes a drawable.", // tooltip ResourceFolderType.DRAWABLE, // folder type AndroidTargetData.DESCRIPTOR_DRAWABLE, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("Menu", // UI name "An XML file that describes an menu.", // tooltip ResourceFolderType.MENU, // folder type SdkConstants.TAG_MENU, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("Color List", // UI name "An XML file that describes a color state list.", // tooltip ResourceFolderType.COLOR, // folder type AndroidTargetData.DESCRIPTOR_COLOR, // root seed "selector", //$NON-NLS-1$ // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("Property Animation", // UI name "An XML file that describes a property animation", // tooltip ResourceFolderType.ANIMATOR, // folder type AndroidTargetData.DESCRIPTOR_ANIMATOR, // root seed "set", //$NON-NLS-1$ // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 11 // target API level ), new TypeInfo("Tween Animation", // UI name "An XML file that describes a tween animation.", // tooltip ResourceFolderType.ANIM, // folder type AndroidTargetData.DESCRIPTOR_ANIM, // root seed "set", //$NON-NLS-1$ // default root null, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("AppWidget Provider", // UI name "An XML file that describes a widget provider.", // tooltip ResourceFolderType.XML, // folder type AndroidTargetData.DESCRIPTOR_APPWIDGET_PROVIDER, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 3 // target API level ), new TypeInfo("Preference", // UI name "An XML file that describes preferences.", // tooltip ResourceFolderType.XML, // folder type AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed SdkConstants.CLASS_NAME_PREFERENCE_SCREEN, // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 1 // target API level ), new TypeInfo("Searchable", // UI name "An XML file that describes a searchable.", // tooltip ResourceFolderType.XML, // folder type AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed null, // default root SdkConstants.NS_RESOURCES, // xmlns null, // default attributes 1 // target API level ), // Still missing: Interpolator, Raw and Mipmap. Raw should probably never be in // this menu since it's not often used for creating XML files. }; private NewXmlFileWizard.Values mValues; private ProjectCombo mProjectButton; private Text mFileNameTextField; private Combo mTypeCombo; private IStructuredSelection mInitialSelection; private ResourceFolderType mInitialFolderType; private boolean mInternalTypeUpdate; private TargetChangeListener mSdkTargetChangeListener; private Table mRootTable; private TableViewer mRootTableViewer; // --- UI creation --- /** * Constructs a new {@link NewXmlFileCreationPage}. * <p/> * Called by {@link NewXmlFileWizard#createMainPage}. */ protected NewXmlFileCreationPage(String pageName, NewXmlFileWizard.Values values) { super(pageName); mValues = values; setPageComplete(false); } public void setInitialSelection(IStructuredSelection initialSelection) { mInitialSelection = initialSelection; } public void setInitialFolderType(ResourceFolderType initialType) { mInitialFolderType = initialType; } /** * Called by the parent Wizard to create the UI for this Wizard Page. * * {@inheritDoc} * * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) */ @Override @SuppressWarnings("unused") // SWT constructors have side effects, they aren't unused public void createControl(Composite parent) { // This UI is maintained with WindowBuilder. Composite composite = new Composite(parent, SWT.NULL); composite.setLayout(new GridLayout(2, false /*makeColumnsEqualWidth*/)); composite.setLayoutData(new GridData(GridData.FILL_BOTH)); // label before type radios Label typeLabel = new Label(composite, SWT.NONE); typeLabel.setText("Resource Type:"); mTypeCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY); mTypeCombo.setToolTipText("What type of resource would you like to create?"); mTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); if (mInitialFolderType != null) { mTypeCombo.setEnabled(false); } mTypeCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { TypeInfo type = getSelectedType(); if (type != null) { onSelectType(type); } } }); // separator Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); GridData gd2 = new GridData(GridData.GRAB_HORIZONTAL); gd2.horizontalAlignment = SWT.FILL; gd2.horizontalSpan = 2; separator.setLayoutData(gd2); // Project: [button] String tooltip = "The Android Project where the new resource file will be created."; Label projectLabel = new Label(composite, SWT.NONE); projectLabel.setText("Project:"); projectLabel.setToolTipText(tooltip); ProjectChooserHelper helper = new ProjectChooserHelper(getShell(), null /* filter */); mProjectButton = new ProjectCombo(helper, composite, mValues.project); mProjectButton.setToolTipText(tooltip); mProjectButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mProjectButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { IProject project = mProjectButton.getSelectedProject(); if (project != mValues.project) { changeProject(project); } }; }); // Filename: [text] Label fileLabel = new Label(composite, SWT.NONE); fileLabel.setText("File:"); fileLabel.setToolTipText("The name of the resource file to create."); mFileNameTextField = new Text(composite, SWT.BORDER); mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mFileNameTextField.setToolTipText(tooltip); mFileNameTextField.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mValues.name = mFileNameTextField.getText(); validatePage(); } }); // separator Label rootSeparator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); GridData gd = new GridData(GridData.GRAB_HORIZONTAL); gd.horizontalAlignment = SWT.FILL; gd.horizontalSpan = 2; rootSeparator.setLayoutData(gd); // Root Element: // [TableViewer] Label rootLabel = new Label(composite, SWT.NONE); rootLabel.setText("Root Element:"); new Label(composite, SWT.NONE); mRootTableViewer = new TableViewer(composite, SWT.BORDER | SWT.FULL_SELECTION); mRootTable = mRootTableViewer.getTable(); GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1); tableGridData.heightHint = 200; mRootTable.setLayoutData(tableGridData); setControl(composite); // Update state the first time setErrorMessage(null); setMessage(null); initializeFromSelection(mInitialSelection); updateAvailableTypes(); initializeFromFixedType(); initializeRootValues(); installTargetChangeListener(); initialSelectType(); validatePage(); } private void initialSelectType() { TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); int typeIndex = getTypeComboIndex(mValues.type); if (typeIndex == -1) { typeIndex = 0; } else { assert mValues.type == types[typeIndex]; } mTypeCombo.select(typeIndex); onSelectType(types[typeIndex]); updateRootCombo(types[typeIndex]); } private void installTargetChangeListener() { mSdkTargetChangeListener = new TargetChangeListener() { @Override public IProject getProject() { return mValues.project; } @Override public void reload() { if (mValues.project != null) { changeProject(mValues.project); } } }; AdtPlugin.getDefault().addTargetListener(mSdkTargetChangeListener); } @Override public void dispose() { if (mSdkTargetChangeListener != null) { AdtPlugin.getDefault().removeTargetListener(mSdkTargetChangeListener); mSdkTargetChangeListener = null; } super.dispose(); } /** * Returns the selected root element string, if any. * * @return The selected root element string or null. */ public String getRootElement() { int index = mRootTable.getSelectionIndex(); if (index >= 0) { Object[] roots = (Object[]) mRootTableViewer.getInput(); return roots[index].toString(); } return null; } /** * Called by {@link NewXmlFileWizard} to initialize the page with the selection * received by the wizard -- typically the current user workbench selection. * <p/> * Things we expect to find out from the selection: * <ul> * <li>The project name, valid if it's an android nature.</li> * <li>The current folder, valid if it's a folder under /res</li> * <li>An existing filename, in which case the user will be asked whether to override it.</li> * </ul> * <p/> * The selection can also be set to a {@link Pair} of {@link IProject} and a workspace * resource path (where the resource path does not have to exist yet, such as res/anim/). * * @param selection The selection when the wizard was initiated. */ private boolean initializeFromSelection(IStructuredSelection selection) { if (selection == null) { return false; } // Find the best match in the element list. In case there are multiple selected elements // select the one that provides the most information and assign them a score, // e.g. project=1 + folder=2 + file=4. IProject targetProject = null; String targetWsFolderPath = null; String targetFileName = null; int targetScore = 0; for (Object element : selection.toList()) { if (element instanceof IAdaptable) { IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); IProject project = res != null ? res.getProject() : null; // Is this an Android project? try { if (project == null || !project.hasNature(AdtConstants.NATURE_DEFAULT)) { continue; } } catch (CoreException e) { // checking the nature failed, ignore this resource continue; } int score = 1; // we have a valid project at least IPath wsFolderPath = null; String fileName = null; assert res != null; // Eclipse incorrectly thinks res could be null, so tell it no if (res.getType() == IResource.FOLDER) { wsFolderPath = res.getProjectRelativePath(); } else if (res.getType() == IResource.FILE) { if (SdkUtils.endsWithIgnoreCase(res.getName(), DOT_XML)) { fileName = res.getName(); } wsFolderPath = res.getParent().getProjectRelativePath(); } // Disregard this folder selection if it doesn't point to /res/something if (wsFolderPath != null && wsFolderPath.segmentCount() > 1 && SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { score += 2; } else { wsFolderPath = null; fileName = null; } score += fileName != null ? 4 : 0; if (score > targetScore) { targetScore = score; targetProject = project; targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; targetFileName = fileName; } } else if (element instanceof Pair<?,?>) { // Pair of Project/String @SuppressWarnings("unchecked") Pair<IProject,String> pair = (Pair<IProject,String>)element; targetScore = 1; targetProject = pair.getFirst(); targetWsFolderPath = pair.getSecond(); targetFileName = ""; } } if (targetProject == null) { // Try to figure out the project from the active editor IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null) { IWorkbenchPage page = window.getActivePage(); if (page != null) { IEditorPart activeEditor = page.getActiveEditor(); if (activeEditor instanceof AndroidXmlEditor) { Object input = ((AndroidXmlEditor) activeEditor).getEditorInput(); if (input instanceof FileEditorInput) { FileEditorInput fileInput = (FileEditorInput) input; targetScore = 1; IFile file = fileInput.getFile(); targetProject = file.getProject(); IPath path = file.getParent().getProjectRelativePath(); targetWsFolderPath = path != null ? path.toString() : null; } } } } } if (targetProject == null) { // If we didn't find a default project based on the selection, check how many // open Android projects we can find in the current workspace. If there's only // one, we'll just select it by default. IJavaProject[] projects = AdtUtils.getOpenAndroidProjects(); if (projects != null && projects.length == 1) { targetScore = 1; targetProject = projects[0].getProject(); } } // Now set the UI accordingly if (targetScore > 0) { mValues.project = targetProject; mValues.folderPath = targetWsFolderPath; mProjectButton.setSelectedProject(targetProject); mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ // If the current selection context corresponds to a specific file type, // select it. if (targetWsFolderPath != null) { int pos = targetWsFolderPath.lastIndexOf(WS_SEP_CHAR); if (pos >= 0) { targetWsFolderPath = targetWsFolderPath.substring(pos + 1); } String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP); if (folderSegments.length > 0) { mValues.configuration = FolderConfiguration.getConfig(folderSegments); String folderName = folderSegments[0]; selectTypeFromFolder(folderName); } } } return true; } private void initializeFromFixedType() { if (mInitialFolderType != null) { for (TypeInfo type : sTypes) { if (type.getResFolderType() == mInitialFolderType) { mValues.type = type; updateFolderPath(type); break; } } } } /** * Given a folder name, such as "drawable", select the corresponding type in * the dropdown. */ void selectTypeFromFolder(String folderName) { List<TypeInfo> matches = new ArrayList<TypeInfo>(); boolean selected = false; TypeInfo selectedType = getSelectedType(); for (TypeInfo type : sTypes) { if (type.getResFolderName().equals(folderName)) { matches.add(type); selected |= type == selectedType; } } if (matches.size() == 1) { // If there's only one match, select it if it's not already selected if (!selected) { selectType(matches.get(0)); } } else if (matches.size() > 1) { // There are multiple type candidates for this folder. This can happen // for /res/xml for example. Check to see if one of them is currently // selected. If yes, leave the selection unchanged. If not, deselect all type. if (!selected) { selectType(null); } } else { // Nothing valid was selected. selectType(null); } } /** * Initialize the root values of the type infos based on the current framework values. */ private void initializeRootValues() { IProject project = mValues.project; for (TypeInfo type : sTypes) { // Clear all the roots for this type ArrayList<String> roots = type.getRoots(); if (roots.size() > 0) { roots.clear(); } // depending of the type of the seed, initialize the root in different ways Object rootSeed = type.getRootSeed(); if (rootSeed instanceof String) { // The seed is a single string, Add it as-is. roots.add((String) rootSeed); } else if (rootSeed instanceof String[]) { // The seed is an array of strings. Add them as-is. for (String value : (String[]) rootSeed) { roots.add(value); } } else if (rootSeed instanceof Integer && project != null) { // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_* // In this case add all the children element descriptors defined, recursively, // and avoid infinite recursion by keeping track of what has already been added. // Note: if project is null, the root list will be empty since it has been // cleared above. // get the AndroidTargetData from the project IAndroidTarget target = null; AndroidTargetData data = null; target = Sdk.getCurrent().getTarget(project); if (target == null) { // A project should have a target. The target can be missing if the project // is an old project for which a target hasn't been affected or if the // target no longer exists in this SDK. Simply log the error and dismiss. AdtPlugin.log(IStatus.INFO, "NewXmlFile wizard: no platform target for project %s", //$NON-NLS-1$ project.getName()); continue; } else { data = Sdk.getCurrent().getTargetData(target); if (data == null) { // We should have both a target and its data. // However if the wizard is invoked whilst the platform is still being // loaded we can end up in a weird case where we have a target but it // doesn't have any data yet. // Lets log a warning and silently ignore this root. AdtPlugin.log(IStatus.INFO, "NewXmlFile wizard: no data for target %s, project %s", //$NON-NLS-1$ target.getName(), project.getName()); continue; } } IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); ElementDescriptor descriptor = provider.getDescriptor(); if (descriptor != null) { HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>(); initRootElementDescriptor(roots, descriptor, visited); } // Sort alphabetically. Collections.sort(roots); } } } /** * Helper method to recursively insert all XML names for the given {@link ElementDescriptor} * into the roots array list. Keeps track of visited nodes to avoid infinite recursion. * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic * and not a valid root element. */ private void initRootElementDescriptor(ArrayList<String> roots, ElementDescriptor desc, HashSet<ElementDescriptor> visited) { if (!(desc instanceof DocumentDescriptor)) { String xmlName = desc.getXmlName(); if (xmlName != null && xmlName.length() > 0) { roots.add(xmlName); } } visited.add(desc); for (ElementDescriptor child : desc.getChildren()) { if (!visited.contains(child)) { initRootElementDescriptor(roots, child, visited); } } } /** * Changes mProject to the given new project and update the UI accordingly. * <p/> * Note that this does not check if the new project is the same as the current one * on purpose, which allows a project to be updated when its target has changed or * when targets are loaded in the background. */ private void changeProject(IProject newProject) { mValues.project = newProject; // enable types based on new API level updateAvailableTypes(); initialSelectType(); // update the folder name based on API level updateFolderPath(mValues.type); // update the Type with the new descriptors. initializeRootValues(); // update the combo updateRootCombo(mValues.type); validatePage(); } private void onSelectType(TypeInfo type) { // Do nothing if this is an internal modification or if the widget has been // deselected. if (mInternalTypeUpdate) { return; } mValues.type = type; if (type == null) { return; } // update the combo updateRootCombo(type); // update the folder path updateFolderPath(type); validatePage(); } /** Updates the selected type in the type dropdown control */ private void setSelectedType(TypeInfo type) { TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); if (types != null) { for (int i = 0, n = types.length; i < n; i++) { if (types[i] == type) { mTypeCombo.select(i); break; } } } } /** Returns the selected type in the type dropdown control */ private TypeInfo getSelectedType() { int index = mTypeCombo.getSelectionIndex(); if (index != -1) { TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); return types[index]; } return null; } /** Returns the selected index in the type dropdown control */ private int getTypeComboIndex(TypeInfo type) { TypeInfo[] types = (TypeInfo[]) mTypeCombo.getData(); for (int i = 0, n = types.length; i < n; i++) { if (type == types[i]) { return i; } } return -1; } /** Updates the folder path to reflect the given type */ private void updateFolderPath(TypeInfo type) { String wsFolderPath = mValues.folderPath; String newPath = null; FolderConfiguration config = mValues.configuration; ResourceQualifier qual = config.getInvalidQualifier(); if (qual == null) { // The configuration is valid. Reformat the folder path using the canonical // value from the configuration. newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); } else { // The configuration is invalid. We still update the path but this time // do it manually on the string. if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { wsFolderPath = wsFolderPath.replaceFirst( "^(" + RES_FOLDER_ABS +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$ "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$ } else { newPath = RES_FOLDER_ABS + config.getFolderName(type.getResFolderType()); } } if (newPath != null && !newPath.equals(wsFolderPath)) { mValues.folderPath = newPath; } } /** * Helper method that fills the values of the "root element" combo box based * on the currently selected type radio button. Also disables the combo is there's * only one choice. Always select the first root element for the given type. * * @param type The currently selected {@link TypeInfo}, or null */ private void updateRootCombo(TypeInfo type) { IBaseLabelProvider labelProvider = new ColumnLabelProvider() { @Override public Image getImage(Object element) { return IconFactory.getInstance().getIcon(element.toString()); } }; mRootTableViewer.setContentProvider(new ArrayContentProvider()); mRootTableViewer.setLabelProvider(labelProvider); if (type != null) { // get the list of roots. The list can be empty but not null. ArrayList<String> roots = type.getRoots(); mRootTableViewer.setInput(roots.toArray()); int index = 0; // default is to select the first one String defaultRoot = type.getDefaultRoot(mValues.project); if (defaultRoot != null) { index = roots.indexOf(defaultRoot); } mRootTable.select(index < 0 ? 0 : index); mRootTable.showSelection(); } } /** * Helper method to select the current type in the type dropdown * * @param type The TypeInfo matching the radio button to selected or null to deselect them all. */ private void selectType(TypeInfo type) { mInternalTypeUpdate = true; mValues.type = type; if (type == null) { if (mTypeCombo.getSelectionIndex() != -1) { mTypeCombo.deselect(mTypeCombo.getSelectionIndex()); } } else { setSelectedType(type); } updateRootCombo(type); mInternalTypeUpdate = false; } /** * Add the available types in the type combobox, based on whether they are available * for the current SDK. * <p/> * A type is available either if: * - if mProject is null, API level 1 is considered valid * - if mProject is !null, the project->target->API must be >= to the type's API level. */ private void updateAvailableTypes() { IProject project = mValues.project; IAndroidTarget target = project != null ? Sdk.getCurrent().getTarget(project) : null; int currentApiLevel = 1; if (target != null) { currentApiLevel = target.getVersion().getApiLevel(); } List<String> items = new ArrayList<String>(sTypes.length); List<TypeInfo> types = new ArrayList<TypeInfo>(sTypes.length); for (int i = 0, n = sTypes.length; i < n; i++) { TypeInfo type = sTypes[i]; if (type.getTargetApiLevel() <= currentApiLevel) { items.add(type.getUiName()); types.add(type); } } mTypeCombo.setItems(items.toArray(new String[items.size()])); mTypeCombo.setData(types.toArray(new TypeInfo[types.size()])); } /** * Validates the fields, displays errors and warnings. * Enables the finish button if there are no errors. */ private void validatePage() { String error = null; String warning = null; // -- validate type TypeInfo type = mValues.type; if (error == null) { if (type == null) { error = "One of the types must be selected (e.g. layout, values, etc.)"; } } // -- validate project if (mValues.project == null) { error = "Please select an Android project."; } // -- validate type API level if (error == null) { IAndroidTarget target = Sdk.getCurrent().getTarget(mValues.project); int currentApiLevel = 1; if (target != null) { currentApiLevel = target.getVersion().getApiLevel(); } assert type != null; if (type.getTargetApiLevel() > currentApiLevel) { error = "The API level of the selected type (e.g. AppWidget, etc.) is not " + "compatible with the API level of the project."; } } // -- validate filename if (error == null) { String fileName = mValues.getFileName(); assert type != null; ResourceFolderType folderType = type.getResFolderType(); error = ResourceNameValidator.create(true, folderType).isValid(fileName); } // -- validate destination file doesn't exist if (error == null) { IFile file = mValues.getDestinationFile(); if (file != null && file.exists()) { warning = "The destination file already exists"; } } // -- update UI & enable finish if there's no error setPageComplete(error == null); if (error != null) { setMessage(error, IMessageProvider.ERROR); } else if (warning != null) { setMessage(warning, IMessageProvider.WARNING); } else { setErrorMessage(null); setMessage(null); } } /** * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if * not found * * @param folderType the {@link ResourceFolderType} to look for * @return the corresponding {@link TypeInfo} */ static TypeInfo getTypeInfo(ResourceFolderType folderType) { for (TypeInfo typeInfo : sTypes) { if (typeInfo.getResFolderType() == folderType) { return typeInfo; } } return null; } }