blob: 7a91150508537fb87fb84210edd8a630f74e42d7 [file] [log] [blame]
/*
* 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.editors.wizards;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.sdk.Sdk;
import com.android.ide.eclipse.common.AndroidConstants;
import com.android.ide.eclipse.common.project.ProjectChooserHelper;
import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors;
import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.ConfigurationState;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
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.core.runtime.Path;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jface.viewers.IStructuredSelection;
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.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
/**
* This is the single 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, the resource folder, resource type and file name.
*/
class NewXmlFileCreationPage extends WizardPage {
/**
* 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 Button mWidget;
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;
}
/** Sets the radio button associate with the resource type. Can be null. */
void setWidget(Button widget) {
mWidget = widget;
}
/** Returns the radio button associate with the resource type. Can be null. */
Button getWidget() {
return mWidget;
}
/**
* 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. */
String getDefaultRoot() {
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.
*/
String getDefaultAttrs() {
return mDefaultAttrs;
}
/**
* The minimum API level required by the current SDK target to support this feature.
*/
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
"LinearLayout", // default root
SdkConstants.NS_RESOURCES, // xmlns
"android:layout_width=\"wrap_content\"\n" + // default attributes
"android:layout_height=\"wrap_content\"",
1 // target API level
),
new TypeInfo("Values", // UI name
"An XML file with simple values: colors, strings, dimensions, etc.", // tooltip
ResourceFolderType.VALUES, // folder type
ResourcesDescriptors.ROOT_ELEMENT, // root seed
null, // default root
null, // 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
MenuDescriptors.MENU_ROOT_ELEMENT, // root seed
null, // default root
SdkConstants.NS_RESOURCES, // 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
AndroidConstants.CLASS_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
),
new TypeInfo("Animation", // UI name
"An XML file that describes an animation.", // tooltip
ResourceFolderType.ANIM, // folder type
// TODO reuse constants if we ever make an editor with descriptors for animations
new String[] { // root seed
"set", //$NON-NLS-1$
"alpha", //$NON-NLS-1$
"scale", //$NON-NLS-1$
"translate", //$NON-NLS-1$
"rotate" //$NON-NLS-1$
},
"set", //$NON-NLS-1$ // default root
null, // xmlns
null, // default attributes
1 // target API level
),
};
/** Number of columns in the grid layout */
final static int NUM_COL = 4;
/** Absolute destination folder root, e.g. "/res/" */
private static final String RES_FOLDER_ABS = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
/** Relative destination folder root, e.g. "res/" */
private static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
private IProject mProject;
private Text mProjectTextField;
private Button mProjectBrowseButton;
private Text mFileNameTextField;
private Text mWsFolderPathTextField;
private Combo mRootElementCombo;
private IStructuredSelection mInitialSelection;
private ConfigurationSelector mConfigSelector;
private FolderConfiguration mTempConfig = new FolderConfiguration();
private boolean mInternalWsFolderPathUpdate;
private boolean mInternalTypeUpdate;
private boolean mInternalConfigSelectorUpdate;
private ProjectChooserHelper mProjectChooserHelper;
// --- UI creation ---
/**
* Constructs a new {@link NewXmlFileCreationPage}.
* <p/>
* Called by {@link NewXmlFileWizard#createMainPage()}.
*/
protected NewXmlFileCreationPage(String pageName) {
super(pageName);
setPageComplete(false);
}
public void setInitialSelection(IStructuredSelection initialSelection) {
mInitialSelection = initialSelection;
}
/**
* 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)
*/
public void createControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setFont(parent.getFont());
initializeDialogUnits(parent);
composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/));
composite.setLayoutData(new GridData(GridData.FILL_BOTH));
createProjectGroup(composite);
createTypeGroup(composite);
createRootGroup(composite);
// Show description the first time
setErrorMessage(null);
setMessage(null);
setControl(composite);
// Update state the first time
initializeFromSelection(mInitialSelection);
initializeRootValues();
enableTypesBasedOnApi();
validatePage();
}
/**
* Returns the target project or null.
*/
public IProject getProject() {
return mProject;
}
/**
* Returns the destination filename or an empty string.
*/
public String getFileName() {
return mFileNameTextField == null ? "" : mFileNameTextField.getText(); //$NON-NLS-1$
}
/**
* Returns the destination folder path relative to the project or an empty string.
*/
public String getWsFolderPath() {
return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$
}
/**
* Returns an {@link IFile} on the destination file.
* <p/>
* Uses {@link #getProject()}, {@link #getWsFolderPath()} and {@link #getFileName()}.
* <p/>
* Returns null if the project, filename or folder are invalid and the destination file
* cannot be determined.
* <p/>
* The {@link IFile} is a resource. There might or might not be an actual real file.
*/
public IFile getDestinationFile() {
IProject project = getProject();
String wsFolderPath = getWsFolderPath();
String fileName = getFileName();
if (project != null && wsFolderPath.length() > 0 && fileName.length() > 0) {
IPath dest = new Path(wsFolderPath).append(fileName);
IFile file = project.getFile(dest);
return file;
}
return null;
}
/**
* Returns the {@link TypeInfo} for the currently selected type radio button.
* Returns null if no radio button is selected.
*
* @return A {@link TypeInfo} or null.
*/
public TypeInfo getSelectedType() {
TypeInfo type = null;
for (TypeInfo ti : sTypes) {
if (ti.getWidget().getSelection()) {
type = ti;
break;
}
}
return type;
}
/**
* Returns the selected root element string, if any.
*
* @return The selected root element string or null.
*/
public String getRootElement() {
int index = mRootElementCombo.getSelectionIndex();
if (index >= 0) {
return mRootElementCombo.getItem(index);
}
return null;
}
// --- UI creation ---
/**
* Helper method to create a new GridData with an horizontal span.
*
* @param horizSpan The number of cells for the horizontal span.
* @return A new GridData with the horizontal span.
*/
private GridData newGridData(int horizSpan) {
GridData gd = new GridData();
gd.horizontalSpan = horizSpan;
return gd;
}
/**
* Helper method to create a new GridData with an horizontal span and a style.
*
* @param horizSpan The number of cells for the horizontal span.
* @param style The style, e.g. {@link GridData#FILL_HORIZONTAL}
* @return A new GridData with the horizontal span and the style.
*/
private GridData newGridData(int horizSpan, int style) {
GridData gd = new GridData(style);
gd.horizontalSpan = horizSpan;
return gd;
}
/**
* Helper method that creates an empty cell in the parent composite.
*
* @param parent The parent composite.
*/
private void emptyCell(Composite parent) {
new Label(parent, SWT.NONE);
}
/**
* Pads the parent with empty cells to match the number of columns of the parent grid.
*
* @param parent A grid layout with NUM_COL columns
* @param col The current number of columns used.
* @return 0, the new number of columns used, for convenience.
*/
private int padWithEmptyCells(Composite parent, int col) {
for (; col < NUM_COL; ++col) {
emptyCell(parent);
}
col = 0;
return col;
}
/**
* Creates the project & filename fields.
* <p/>
* The parent must be a GridLayout with NUM_COL colums.
*/
private void createProjectGroup(Composite parent) {
int col = 0;
// project name
String tooltip = "The Android Project where the new resource file will be created.";
Label label = new Label(parent, SWT.NONE);
label.setText("Project");
label.setToolTipText(tooltip);
++col;
mProjectTextField = new Text(parent, SWT.BORDER);
mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mProjectTextField.setToolTipText(tooltip);
mProjectTextField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
onProjectFieldUpdated();
}
});
++col;
mProjectBrowseButton = new Button(parent, SWT.NONE);
mProjectBrowseButton.setText("Browse...");
mProjectBrowseButton.setToolTipText("Allows you to select the Android project to modify.");
mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onProjectBrowse();
}
});
mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
++col;
col = padWithEmptyCells(parent, col);
// file name
tooltip = "The name of the resource file to create.";
label = new Label(parent, SWT.NONE);
label.setText("File");
label.setToolTipText(tooltip);
++col;
mFileNameTextField = new Text(parent, SWT.BORDER);
mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mFileNameTextField.setToolTipText(tooltip);
mFileNameTextField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
validatePage();
}
});
++col;
padWithEmptyCells(parent, col);
}
/**
* Creates the type field, {@link ConfigurationSelector} and the folder field.
* <p/>
* The parent must be a GridLayout with NUM_COL colums.
*/
private void createTypeGroup(Composite parent) {
// separator
Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
// label before type radios
label = new Label(parent, SWT.NONE);
label.setText("What type of resource would you like to create?");
label.setLayoutData(newGridData(NUM_COL));
// display the types on three columns of radio buttons.
emptyCell(parent);
Composite grid = new Composite(parent, SWT.NONE);
padWithEmptyCells(parent, 2);
grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/));
SelectionListener radioListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
// single-click. Only do something if activated.
if (e.getSource() instanceof Button) {
onRadioTypeUpdated((Button) e.getSource());
}
}
};
int n = sTypes.length;
int num_lines = (n + NUM_COL/2) / NUM_COL;
for (int line = 0, k = 0; line < num_lines; line++) {
for (int i = 0; i < NUM_COL; i++, k++) {
if (k < n) {
TypeInfo type = sTypes[k];
Button radio = new Button(grid, SWT.RADIO);
type.setWidget(radio);
radio.setSelection(false);
radio.setText(type.getUiName());
radio.setToolTipText(type.getTooltip());
radio.addSelectionListener(radioListener);
} else {
emptyCell(grid);
}
}
}
// label before configuration selector
label = new Label(parent, SWT.NONE);
label.setText("What type of resource configuration would you like?");
label.setLayoutData(newGridData(NUM_COL));
// configuration selector
emptyCell(parent);
mConfigSelector = new ConfigurationSelector(parent);
GridData gd = newGridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
gd.widthHint = ConfigurationSelector.WIDTH_HINT;
gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
mConfigSelector.setLayoutData(gd);
mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated());
emptyCell(parent);
// folder name
String tooltip = "The folder where the file will be generated, relative to the project.";
label = new Label(parent, SWT.NONE);
label.setText("Folder");
label.setToolTipText(tooltip);
mWsFolderPathTextField = new Text(parent, SWT.BORDER);
mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mWsFolderPathTextField.setToolTipText(tooltip);
mWsFolderPathTextField.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
onWsFolderPathUpdated();
}
});
}
/**
* Creates the root element combo.
* <p/>
* The parent must be a GridLayout with NUM_COL colums.
*/
private void createRootGroup(Composite parent) {
// separator
Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
// label before the root combo
String tooltip = "The root element to create in the XML file.";
label = new Label(parent, SWT.NONE);
label.setText("Select the root element for the XML file:");
label.setLayoutData(newGridData(NUM_COL));
label.setToolTipText(tooltip);
// root combo
emptyCell(parent);
mRootElementCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
mRootElementCombo.setEnabled(false);
mRootElementCombo.select(0);
mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mRootElementCombo.setToolTipText(tooltip);
padWithEmptyCells(parent, 2);
}
/**
* 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>
*
* @param selection The selection when the wizard was initiated.
*/
private void initializeFromSelection(IStructuredSelection selection) {
if (selection == null) {
return;
}
// 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(AndroidConstants.NATURE)) {
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;
if (res.getType() == IResource.FOLDER) {
wsFolderPath = res.getProjectRelativePath();
} else if (res.getType() == IResource.FILE) {
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;
}
}
}
// Now set the UI accordingly
if (targetScore > 0) {
mProject = targetProject;
mProjectTextField.setText(targetProject != null ? targetProject.getName() : ""); //$NON-NLS-1$
mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$
mWsFolderPathTextField.setText(targetWsFolderPath != null ? targetWsFolderPath : ""); //$NON-NLS-1$
}
}
/**
* Initialize the root values of the type infos based on the current framework values.
*/
private void initializeRootValues() {
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 && mProject != 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(mProject);
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$
mProject.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(), mProject.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);
}
}
}
/**
* Callback called when the user edits the project text field.
*/
private void onProjectFieldUpdated() {
String project = mProjectTextField.getText();
// Is this a valid project?
IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
IProject found = null;
for (IJavaProject p : projects) {
if (p.getProject().getName().equals(project)) {
found = p.getProject();
break;
}
}
if (found != mProject) {
changeProject(found);
}
}
/**
* Callback called when the user uses the "Browse Projects" button.
*/
private void onProjectBrowse() {
IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText());
if (p != null) {
changeProject(p.getProject());
mProjectTextField.setText(mProject.getName());
}
}
/**
* Changes mProject to the given new project and update the UI accordingly.
*/
private void changeProject(IProject newProject) {
mProject = newProject;
// enable types based on new API level
enableTypesBasedOnApi();
// update the Type with the new descriptors.
initializeRootValues();
// update the combo
updateRootCombo(getSelectedType());
validatePage();
}
/**
* Callback called when the Folder text field is changed, either programmatically
* or by the user.
*/
private void onWsFolderPathUpdated() {
if (mInternalWsFolderPathUpdate) {
return;
}
String wsFolderPath = mWsFolderPathTextField.getText();
// This is a custom path, we need to sanitize it.
// First it should start with "/res/". Then we need to make sure there are no
// relative paths, things like "../" or "./" or even "//".
wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$
wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$
wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>();
// We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
mInternalWsFolderPathUpdate = true;
mWsFolderPathTextField.setText(wsFolderPath);
mInternalWsFolderPathUpdate = false;
}
if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
if (pos >= 0) {
wsFolderPath = wsFolderPath.substring(0, pos);
}
String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
if (folderSegments.length > 0) {
String folderName = folderSegments[0];
// update config selector
mInternalConfigSelectorUpdate = true;
mConfigSelector.setConfiguration(folderSegments);
mInternalConfigSelectorUpdate = false;
boolean selected = false;
for (TypeInfo type : sTypes) {
if (type.getResFolderName().equals(folderName)) {
matches.add(type);
selected |= type.getWidget().getSelection();
}
}
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);
}
}
}
validatePage();
}
/**
* Callback called when one of the type radio button is changed.
*
* @param typeWidget The type radio button that changed.
*/
private void onRadioTypeUpdated(Button typeWidget) {
// Do nothing if this is an internal modification or if the widget has been
// de-selected.
if (mInternalTypeUpdate || !typeWidget.getSelection()) {
return;
}
// Find type info that has just been enabled.
TypeInfo type = null;
for (TypeInfo ti : sTypes) {
if (ti.getWidget() == typeWidget) {
type = ti;
break;
}
}
if (type == null) {
return;
}
// update the combo
updateRootCombo(type);
// update the folder path
String wsFolderPath = mWsFolderPathTextField.getText();
String newPath = null;
mConfigSelector.getConfiguration(mTempConfig);
ResourceQualifier qual = mTempConfig.getInvalidQualifier();
if (qual == null) {
// The configuration is valid. Reformat the folder path using the canonical
// value from the configuration.
newPath = RES_FOLDER_ABS + mTempConfig.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.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 + mTempConfig.getFolderName(type.getResFolderType());
}
}
if (newPath != null && !newPath.equals(wsFolderPath)) {
mInternalWsFolderPathUpdate = true;
mWsFolderPathTextField.setText(newPath);
mInternalWsFolderPathUpdate = false;
}
validatePage();
}
/**
* 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}. Cannot be null.
*/
private void updateRootCombo(TypeInfo type) {
// reset all the values in the combo
mRootElementCombo.removeAll();
if (type != null) {
// get the list of roots. The list can be empty but not null.
ArrayList<String> roots = type.getRoots();
// enable the combo if there's more than one choice
mRootElementCombo.setEnabled(roots != null && roots.size() > 1);
for (String root : roots) {
mRootElementCombo.add(root);
}
int index = 0; // default is to select the first one
String defaultRoot = type.getDefaultRoot();
if (defaultRoot != null) {
index = roots.indexOf(defaultRoot);
}
mRootElementCombo.select(index < 0 ? 0 : index);
}
}
/**
* Callback called when the configuration has changed in the {@link ConfigurationSelector}.
*/
private class onConfigSelectorUpdated implements Runnable {
public void run() {
if (mInternalConfigSelectorUpdate) {
return;
}
TypeInfo type = getSelectedType();
if (type != null) {
mConfigSelector.getConfiguration(mTempConfig);
StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
sb.append(mTempConfig.getFolderName(type.getResFolderType()));
mInternalWsFolderPathUpdate = true;
mWsFolderPathTextField.setText(sb.toString());
mInternalWsFolderPathUpdate = false;
validatePage();
}
}
}
/**
* Helper method to select on of the type radio buttons.
*
* @param type The TypeInfo matching the radio button to selected or null to deselect them all.
*/
private void selectType(TypeInfo type) {
if (type == null || !type.getWidget().getSelection()) {
mInternalTypeUpdate = true;
for (TypeInfo type2 : sTypes) {
type2.getWidget().setSelection(type2 == type);
}
updateRootCombo(type);
mInternalTypeUpdate = false;
}
}
/**
* Helper method to enable the type radio buttons depending on the current API level.
* <p/>
* A type radio button is enabled 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 enableTypesBasedOnApi() {
IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null;
int currentApiLevel = 1;
if (target != null) {
currentApiLevel = target.getApiVersionNumber();
}
for (TypeInfo type : sTypes) {
type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel);
}
}
/**
* 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 project
if (getProject() == null) {
error = "Please select an Android project.";
}
// -- validate filename
if (error == null) {
String fileName = getFileName();
if (fileName == null || fileName.length() == 0) {
error = "A destination file name is required.";
} else if (!fileName.endsWith(AndroidConstants.DOT_XML)) {
error = String.format("The filename must end with %1$s.", AndroidConstants.DOT_XML);
}
}
// -- validate type
if (error == null) {
TypeInfo type = getSelectedType();
if (type == null) {
error = "One of the types must be selected (e.g. layout, values, etc.)";
}
}
// -- validate type API level
if (error == null) {
IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
int currentApiLevel = 1;
if (target != null) {
currentApiLevel = target.getApiVersionNumber();
}
TypeInfo type = getSelectedType();
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 folder configuration
if (error == null) {
ConfigurationState state = mConfigSelector.getState();
if (state == ConfigurationState.INVALID_CONFIG) {
ResourceQualifier qual = mConfigSelector.getInvalidQualifier();
if (qual != null) {
error = String.format("The qualifier '%1$s' is invalid in the folder configuration.",
qual.getName());
}
} else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) {
error = "The Region qualifier requires the Language qualifier.";
}
}
// -- validate generated path
if (error == null) {
String wsFolderPath = getWsFolderPath();
if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) {
error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS);
}
}
// -- validate destination file doesn't exist
if (error == null) {
IFile file = 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, WizardPage.ERROR);
} else if (warning != null) {
setMessage(warning, WizardPage.WARNING);
} else {
setErrorMessage(null);
setMessage(null);
}
}
}