Merge branch 'readonly-p4-master'
This commit is contained in:
committed by
The Android Open Source Project
commit
f2ee1e4660
@@ -17,72 +17,34 @@
|
|||||||
package com.android.ide.eclipse.adt.refactorings.extractstring;
|
package com.android.ide.eclipse.adt.refactorings.extractstring;
|
||||||
|
|
||||||
|
|
||||||
import com.android.ide.eclipse.adt.ui.ConfigurationSelector;
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl;
|
||||||
import com.android.ide.eclipse.common.AndroidConstants;
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl.INewStringPageCallback;
|
||||||
import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl.ValidationStatus;
|
||||||
import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
|
|
||||||
import com.android.sdklib.SdkConstants;
|
|
||||||
|
|
||||||
import org.eclipse.core.resources.IFolder;
|
|
||||||
import org.eclipse.core.resources.IProject;
|
import org.eclipse.core.resources.IProject;
|
||||||
import org.eclipse.core.resources.IResource;
|
|
||||||
import org.eclipse.core.runtime.CoreException;
|
|
||||||
import org.eclipse.jface.wizard.IWizardPage;
|
import org.eclipse.jface.wizard.IWizardPage;
|
||||||
import org.eclipse.jface.wizard.WizardPage;
|
|
||||||
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
|
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
|
||||||
import org.eclipse.swt.SWT;
|
import org.eclipse.swt.SWT;
|
||||||
import org.eclipse.swt.events.ModifyEvent;
|
import org.eclipse.swt.events.ModifyEvent;
|
||||||
import org.eclipse.swt.events.ModifyListener;
|
import org.eclipse.swt.events.ModifyListener;
|
||||||
import org.eclipse.swt.layout.GridData;
|
import org.eclipse.swt.layout.GridData;
|
||||||
import org.eclipse.swt.layout.GridLayout;
|
import org.eclipse.swt.layout.GridLayout;
|
||||||
import org.eclipse.swt.widgets.Combo;
|
|
||||||
import org.eclipse.swt.widgets.Composite;
|
import org.eclipse.swt.widgets.Composite;
|
||||||
import org.eclipse.swt.widgets.Group;
|
import org.eclipse.swt.widgets.Group;
|
||||||
import org.eclipse.swt.widgets.Label;
|
import org.eclipse.swt.widgets.Label;
|
||||||
import org.eclipse.swt.widgets.Text;
|
import org.eclipse.swt.widgets.Text;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see ExtractStringRefactoring
|
* @see ExtractStringRefactoring
|
||||||
*/
|
*/
|
||||||
class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
|
class ExtractStringInputPage extends UserInputWizardPage
|
||||||
|
implements IWizardPage, INewStringPageCallback {
|
||||||
|
|
||||||
/** Last res file path used, shared across the session instances but specific to the
|
private NewStringBaseImpl mImpl;
|
||||||
* current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */
|
|
||||||
private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>();
|
|
||||||
|
|
||||||
/** The project where the user selection happened. */
|
|
||||||
private final IProject mProject;
|
|
||||||
|
|
||||||
/** Field displaying the user-selected string to be replaced. */
|
|
||||||
private Label mStringLabel;
|
|
||||||
/** Test field where the user enters the new ID to be generated or replaced with. */
|
|
||||||
private Text mNewIdTextField;
|
|
||||||
/** The configuration selector, to select the resource path of the XML file. */
|
|
||||||
private ConfigurationSelector mConfigSelector;
|
|
||||||
/** The combo to display the existing XML files or enter a new one. */
|
|
||||||
private Combo mResFileCombo;
|
|
||||||
|
|
||||||
/** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
|
|
||||||
* a leaf file name ending with .xml */
|
|
||||||
private static final Pattern RES_XML_FILE_REGEX = Pattern.compile(
|
|
||||||
"/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$
|
|
||||||
/** 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 static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml";
|
|
||||||
|
|
||||||
public ExtractStringInputPage(IProject project) {
|
public ExtractStringInputPage(IProject project) {
|
||||||
super("ExtractStringInputPage"); //$NON-NLS-1$
|
super("ExtractStringInputPage"); //$NON-NLS-1$
|
||||||
mProject = project;
|
mImpl = new NewStringBaseImpl(project, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,17 +54,12 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
|
|||||||
* {@link ExtractStringRefactoring}.
|
* {@link ExtractStringRefactoring}.
|
||||||
*/
|
*/
|
||||||
public void createControl(Composite parent) {
|
public void createControl(Composite parent) {
|
||||||
|
|
||||||
Composite content = new Composite(parent, SWT.NONE);
|
Composite content = new Composite(parent, SWT.NONE);
|
||||||
|
|
||||||
GridLayout layout = new GridLayout();
|
GridLayout layout = new GridLayout();
|
||||||
layout.numColumns = 1;
|
layout.numColumns = 1;
|
||||||
content.setLayout(layout);
|
content.setLayout(layout);
|
||||||
|
|
||||||
createStringReplacementGroup(content);
|
mImpl.createControl(content);
|
||||||
createResFileGroup(content);
|
|
||||||
|
|
||||||
validatePage();
|
|
||||||
setControl(content);
|
setControl(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +68,9 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
|
|||||||
* and by which options.
|
* and by which options.
|
||||||
*
|
*
|
||||||
* @param content A composite with a 1-column grid layout
|
* @param content A composite with a 1-column grid layout
|
||||||
|
* @return The {@link Text} field for the new String ID name.
|
||||||
*/
|
*/
|
||||||
private void createStringReplacementGroup(Composite content) {
|
public Text createStringGroup(Composite content) {
|
||||||
|
|
||||||
final ExtractStringRefactoring ref = getOurRefactoring();
|
final ExtractStringRefactoring ref = getOurRefactoring();
|
||||||
|
|
||||||
@@ -124,16 +82,27 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
|
|||||||
layout.numColumns = 2;
|
layout.numColumns = 2;
|
||||||
group.setLayout(layout);
|
group.setLayout(layout);
|
||||||
|
|
||||||
// line: String found in selection
|
// line: Textfield for string value (based on selection, if any)
|
||||||
|
|
||||||
Label label = new Label(group, SWT.NONE);
|
Label label = new Label(group, SWT.NONE);
|
||||||
label.setText("String:");
|
label.setText("String:");
|
||||||
|
|
||||||
String selectedString = ref.getTokenString();
|
String selectedString = ref.getTokenString();
|
||||||
|
|
||||||
mStringLabel = new Label(group, SWT.NONE);
|
final Text stringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
|
||||||
mStringLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
stringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
mStringLabel.setText(selectedString != null ? selectedString : "");
|
stringValueField.setText(selectedString != null ? selectedString : ""); //$NON-NLS-1$
|
||||||
|
|
||||||
|
ref.setNewStringValue(stringValueField.getText());
|
||||||
|
|
||||||
|
stringValueField.addModifyListener(new ModifyListener() {
|
||||||
|
public void modifyText(ModifyEvent e) {
|
||||||
|
if (mImpl.validatePage()) {
|
||||||
|
ref.setNewStringValue(stringValueField.getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// TODO provide an option to replace all occurences of this string instead of
|
// TODO provide an option to replace all occurences of this string instead of
|
||||||
// just the one.
|
// just the one.
|
||||||
@@ -143,74 +112,31 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
|
|||||||
label = new Label(group, SWT.NONE);
|
label = new Label(group, SWT.NONE);
|
||||||
label.setText("Replace by R.string.");
|
label.setText("Replace by R.string.");
|
||||||
|
|
||||||
mNewIdTextField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
|
final Text stringIdField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
|
||||||
mNewIdTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
stringIdField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
mNewIdTextField.setText(guessId(selectedString));
|
stringIdField.setText(guessId(selectedString));
|
||||||
|
|
||||||
ref.setReplacementStringId(mNewIdTextField.getText().trim());
|
ref.setNewStringId(stringIdField.getText().trim());
|
||||||
|
|
||||||
mNewIdTextField.addModifyListener(new ModifyListener() {
|
stringIdField.addModifyListener(new ModifyListener() {
|
||||||
public void modifyText(ModifyEvent e) {
|
public void modifyText(ModifyEvent e) {
|
||||||
if (validatePage()) {
|
if (mImpl.validatePage()) {
|
||||||
ref.setReplacementStringId(mNewIdTextField.getText().trim());
|
ref.setNewStringId(stringIdField.getText().trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return stringIdField;
|
||||||
* Creates the lower group with the fields to choose the resource confirmation and
|
|
||||||
* the target XML file.
|
|
||||||
*
|
|
||||||
* @param content A composite with a 1-column grid layout
|
|
||||||
*/
|
|
||||||
private void createResFileGroup(Composite content) {
|
|
||||||
|
|
||||||
Group group = new Group(content, SWT.NONE);
|
|
||||||
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
|
||||||
group.setText("XML resource to edit");
|
|
||||||
|
|
||||||
GridLayout layout = new GridLayout();
|
|
||||||
layout.numColumns = 2;
|
|
||||||
group.setLayout(layout);
|
|
||||||
|
|
||||||
// line: selection of the res config
|
|
||||||
|
|
||||||
Label label;
|
|
||||||
label = new Label(group, SWT.NONE);
|
|
||||||
label.setText("Configuration:");
|
|
||||||
|
|
||||||
mConfigSelector = new ConfigurationSelector(group);
|
|
||||||
GridData gd = new GridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
|
|
||||||
gd.widthHint = ConfigurationSelector.WIDTH_HINT;
|
|
||||||
gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
|
|
||||||
mConfigSelector.setLayoutData(gd);
|
|
||||||
OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
|
|
||||||
mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
|
|
||||||
|
|
||||||
// line: selection of the output file
|
|
||||||
|
|
||||||
label = new Label(group, SWT.NONE);
|
|
||||||
label.setText("Resource file:");
|
|
||||||
|
|
||||||
mResFileCombo = new Combo(group, SWT.DROP_DOWN);
|
|
||||||
mResFileCombo.select(0);
|
|
||||||
mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
|
||||||
mResFileCombo.addModifyListener(onConfigSelectorUpdated);
|
|
||||||
|
|
||||||
// set output file name to the last one used
|
|
||||||
|
|
||||||
String projPath = mProject.getFullPath().toPortableString();
|
|
||||||
String filePath = sLastResFilePath.get(projPath);
|
|
||||||
|
|
||||||
mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
|
|
||||||
onConfigSelectorUpdated.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to guess a suitable new XML ID based on the selected string.
|
* Utility method to guess a suitable new XML ID based on the selected string.
|
||||||
*/
|
*/
|
||||||
private String guessId(String text) {
|
private String guessId(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return ""; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
// make lower case
|
// make lower case
|
||||||
text = text.toLowerCase();
|
text = text.toLowerCase();
|
||||||
|
|
||||||
@@ -231,247 +157,8 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage
|
|||||||
return (ExtractStringRefactoring) getRefactoring();
|
return (ExtractStringRefactoring) getRefactoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void postValidatePage(ValidationStatus status) {
|
||||||
* Validates fields of the wizard input page. Displays errors as appropriate and
|
ExtractStringRefactoring ref = getOurRefactoring();
|
||||||
* enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}.
|
ref.setTargetFile(mImpl.getResFileProjPath());
|
||||||
*
|
|
||||||
* @return True if the page has been positively validated. It may still have warnings.
|
|
||||||
*/
|
|
||||||
private boolean validatePage() {
|
|
||||||
boolean success = true;
|
|
||||||
|
|
||||||
// Analyze fatal errors.
|
|
||||||
|
|
||||||
String text = mNewIdTextField.getText().trim();
|
|
||||||
if (text == null || text.length() < 1) {
|
|
||||||
setErrorMessage("Please provide a resource ID to replace with.");
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
|
||||||
char c = text.charAt(i);
|
|
||||||
boolean ok = i == 0 ?
|
|
||||||
Character.isJavaIdentifierStart(c) :
|
|
||||||
Character.isJavaIdentifierPart(c);
|
|
||||||
if (!ok) {
|
|
||||||
setErrorMessage(String.format(
|
|
||||||
"The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.",
|
|
||||||
c, i+1));
|
|
||||||
success = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String resFile = mResFileCombo.getText();
|
|
||||||
if (success) {
|
|
||||||
if (resFile == null || resFile.length() == 0) {
|
|
||||||
setErrorMessage("A resource file name is required.");
|
|
||||||
success = false;
|
|
||||||
} else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) {
|
|
||||||
setErrorMessage("The XML file name is not valid.");
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analyze info & warnings.
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
ExtractStringRefactoring ref = getOurRefactoring();
|
|
||||||
|
|
||||||
ref.setTargetFile(resFile);
|
|
||||||
sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
|
|
||||||
|
|
||||||
if (ref.isResIdDuplicate(resFile, text)) {
|
|
||||||
setMessage(
|
|
||||||
String.format("There's already a string item called '%1$s' in %2$s.",
|
|
||||||
text, resFile),
|
|
||||||
WizardPage.WARNING);
|
|
||||||
} else if (mProject.findMember(resFile) == null) {
|
|
||||||
setMessage(
|
|
||||||
String.format("File %2$s does not exist and will be created.",
|
|
||||||
text, resFile),
|
|
||||||
WizardPage.INFORMATION);
|
|
||||||
} else {
|
|
||||||
setMessage(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPageComplete(success);
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
|
|
||||||
|
|
||||||
/** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
|
|
||||||
private final Pattern mPathRegex = Pattern.compile(
|
|
||||||
"(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$
|
|
||||||
|
|
||||||
/** Temporary config object used to retrieve the Config Selector value. */
|
|
||||||
private FolderConfiguration mTempConfig = new FolderConfiguration();
|
|
||||||
|
|
||||||
private HashMap<String, TreeSet<String>> mFolderCache =
|
|
||||||
new HashMap<String, TreeSet<String>>();
|
|
||||||
private String mLastFolderUsedInCombo = null;
|
|
||||||
private boolean mInternalConfigChange;
|
|
||||||
private boolean mInternalFileComboChange;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when the {@link ConfigurationSelector} has been changed.
|
|
||||||
* <p/>
|
|
||||||
* The callback does the following:
|
|
||||||
* <ul>
|
|
||||||
* <li> Examine the current file name to retrieve the XML filename, if any.
|
|
||||||
* <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/).
|
|
||||||
* <li> Examine the path to retrieve all the files in it. Keep those in a local cache.
|
|
||||||
* <li> If the XML filename from step 1 is not in the file list, it's a custom file name.
|
|
||||||
* Insert it and sort it.
|
|
||||||
* <li> Re-populate the file combo with all the choices.
|
|
||||||
* <li> Select the original XML file.
|
|
||||||
*/
|
|
||||||
public void run() {
|
|
||||||
if (mInternalConfigChange) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get current leafname, if any
|
|
||||||
String leafName = "";
|
|
||||||
String currPath = mResFileCombo.getText();
|
|
||||||
Matcher m = mPathRegex.matcher(currPath);
|
|
||||||
if (m.matches()) {
|
|
||||||
// Note: groups 1 and 2 cannot be null.
|
|
||||||
leafName = m.group(2);
|
|
||||||
currPath = m.group(1);
|
|
||||||
} else {
|
|
||||||
// There was a path but it was invalid. Ignore it.
|
|
||||||
currPath = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// recreate the res path from the current configuration
|
|
||||||
mConfigSelector.getConfiguration(mTempConfig);
|
|
||||||
StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
|
|
||||||
sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
|
|
||||||
sb.append('/');
|
|
||||||
|
|
||||||
String newPath = sb.toString();
|
|
||||||
if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
|
|
||||||
// Path has not changed. No need to reload.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all the files at the new path
|
|
||||||
|
|
||||||
TreeSet<String> filePaths = mFolderCache.get(newPath);
|
|
||||||
|
|
||||||
if (filePaths == null) {
|
|
||||||
filePaths = new TreeSet<String>();
|
|
||||||
|
|
||||||
IFolder folder = mProject.getFolder(newPath);
|
|
||||||
if (folder != null && folder.exists()) {
|
|
||||||
try {
|
|
||||||
for (IResource res : folder.members()) {
|
|
||||||
String name = res.getName();
|
|
||||||
if (res.getType() == IResource.FILE && name.endsWith(".xml")) {
|
|
||||||
filePaths.add(newPath + name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (CoreException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mFolderCache.put(newPath, filePaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
currPath = newPath + leafName;
|
|
||||||
if (leafName.length() > 0 && !filePaths.contains(currPath)) {
|
|
||||||
filePaths.add(currPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill the combo
|
|
||||||
try {
|
|
||||||
mInternalFileComboChange = true;
|
|
||||||
|
|
||||||
mResFileCombo.removeAll();
|
|
||||||
|
|
||||||
for (String filePath : filePaths) {
|
|
||||||
mResFileCombo.add(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = -1;
|
|
||||||
if (leafName.length() > 0) {
|
|
||||||
index = mResFileCombo.indexOf(currPath);
|
|
||||||
if (index >= 0) {
|
|
||||||
mResFileCombo.select(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == -1) {
|
|
||||||
mResFileCombo.setText(currPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
mLastFolderUsedInCombo = newPath;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
mInternalFileComboChange = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally validate the whole page
|
|
||||||
validatePage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been
|
|
||||||
* modified.
|
|
||||||
*/
|
|
||||||
public void modifyText(ModifyEvent e) {
|
|
||||||
if (mInternalFileComboChange) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String wsFolderPath = mResFileCombo.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$
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
mInternalFileComboChange = true;
|
|
||||||
mResFileCombo.setText(wsFolderPath);
|
|
||||||
mInternalFileComboChange = 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];
|
|
||||||
|
|
||||||
if (folderName != null && !folderName.equals(wsFolderPath)) {
|
|
||||||
// update config selector
|
|
||||||
mInternalConfigChange = true;
|
|
||||||
mConfigSelector.setConfiguration(folderSegments);
|
|
||||||
mInternalConfigChange = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
package com.android.ide.eclipse.adt.refactorings.extractstring;
|
package com.android.ide.eclipse.adt.refactorings.extractstring;
|
||||||
|
|
||||||
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringHelper;
|
||||||
import com.android.ide.eclipse.common.AndroidConstants;
|
import com.android.ide.eclipse.common.AndroidConstants;
|
||||||
import com.android.ide.eclipse.common.project.AndroidManifestParser;
|
import com.android.ide.eclipse.common.project.AndroidManifestParser;
|
||||||
import com.android.ide.eclipse.common.project.AndroidXPathFactory;
|
|
||||||
|
|
||||||
import org.eclipse.core.resources.IFile;
|
import org.eclipse.core.resources.IFile;
|
||||||
import org.eclipse.core.resources.IProject;
|
import org.eclipse.core.resources.IProject;
|
||||||
@@ -64,8 +64,6 @@ import org.eclipse.text.edits.MultiTextEdit;
|
|||||||
import org.eclipse.text.edits.ReplaceEdit;
|
import org.eclipse.text.edits.ReplaceEdit;
|
||||||
import org.eclipse.text.edits.TextEdit;
|
import org.eclipse.text.edits.TextEdit;
|
||||||
import org.eclipse.text.edits.TextEditGroup;
|
import org.eclipse.text.edits.TextEditGroup;
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -73,14 +71,9 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.xml.xpath.XPath;
|
|
||||||
import javax.xml.xpath.XPathConstants;
|
|
||||||
import javax.xml.xpath.XPathExpressionException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This refactoring extracts a string from a file and replaces it by an Android resource ID
|
* This refactoring extracts a string from a file and replaces it by an Android resource ID
|
||||||
* such as R.string.foo.
|
* such as R.string.foo.
|
||||||
@@ -105,8 +98,8 @@ import javax.xml.xpath.XPathExpressionException;
|
|||||||
* <li> On success, the wizard is shown, which let the user input the new ID to use.
|
* <li> On success, the wizard is shown, which let the user input the new ID to use.
|
||||||
* <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
|
* <li> The wizard sets the user input values into this refactoring instance, e.g. the new string
|
||||||
* ID, the XML file to update, etc. The wizard does use the utility method
|
* ID, the XML file to update, etc. The wizard does use the utility method
|
||||||
* {@link #isResIdDuplicate(String, String)} to check whether the new ID is already defined
|
* {@link NewStringHelper#isResIdDuplicate(IProject, String, String)} to check whether the new
|
||||||
* in the target XML file.
|
* ID is already defined in the target XML file.
|
||||||
* <li> Once Preview or Finish is selected in the wizard, the
|
* <li> Once Preview or Finish is selected in the wizard, the
|
||||||
* {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
|
* {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
|
||||||
* and compute the actual changes.
|
* and compute the actual changes.
|
||||||
@@ -127,60 +120,106 @@ import javax.xml.xpath.XPathExpressionException;
|
|||||||
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
|
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
class ExtractStringRefactoring extends Refactoring {
|
public class ExtractStringRefactoring extends Refactoring {
|
||||||
|
|
||||||
/** The file model being manipulated. */
|
private enum Mode {
|
||||||
|
EDIT_SOURCE,
|
||||||
|
MAKE_ID,
|
||||||
|
MAKE_NEW_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The {@link Mode} of operation of the refactoring. */
|
||||||
|
private final Mode mMode;
|
||||||
|
/** The file model being manipulated.
|
||||||
|
* Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
|
||||||
private final IFile mFile;
|
private final IFile mFile;
|
||||||
/** The start of the selection in {@link #mFile}. */
|
/** The start of the selection in {@link #mFile}.
|
||||||
|
* Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
|
||||||
private final int mSelectionStart;
|
private final int mSelectionStart;
|
||||||
/** The end of the selection in {@link #mFile}. */
|
/** The end of the selection in {@link #mFile}.
|
||||||
|
* Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */
|
||||||
private final int mSelectionEnd;
|
private final int mSelectionEnd;
|
||||||
|
|
||||||
/** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
|
/** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
|
||||||
private ICompilationUnit mUnit;
|
private ICompilationUnit mUnit;
|
||||||
/** The actual string selected, after UTF characters have been escaped, good for display. */
|
/** The actual string selected, after UTF characters have been escaped, good for display.
|
||||||
|
* Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
|
||||||
private String mTokenString;
|
private String mTokenString;
|
||||||
|
|
||||||
/** The XML string ID selected by the user in the wizard. */
|
/** The XML string ID selected by the user in the wizard. */
|
||||||
private String mXmlStringId;
|
private String mXmlStringId;
|
||||||
|
/** The XML string value. Might be different than the initial selected string. */
|
||||||
|
private String mXmlStringValue;
|
||||||
/** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
|
/** The path of the XML file that will define {@link #mXmlStringId}, selected by the user
|
||||||
* in the wizard. */
|
* in the wizard. */
|
||||||
private String mTargetXmlFileWsPath;
|
private String mTargetXmlFileWsPath;
|
||||||
|
|
||||||
/** A temporary cache of R.string IDs defined by a given xml file. The key is the
|
|
||||||
* project path of the file, the data is a set of known string Ids for that file. */
|
|
||||||
private HashMap<String,HashSet<String>> mResIdCache;
|
|
||||||
/** An instance of XPath, created lazily on demand. */
|
|
||||||
private XPath mXPath;
|
|
||||||
/** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
|
/** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and
|
||||||
* used by {@link #createChange(IProgressMonitor)}. */
|
* used by {@link #createChange(IProgressMonitor)}. */
|
||||||
private ArrayList<Change> mChanges;
|
private ArrayList<Change> mChanges;
|
||||||
|
|
||||||
|
private NewStringHelper mHelper = new NewStringHelper();
|
||||||
|
|
||||||
public ExtractStringRefactoring(Map<String, String> arguments)
|
public ExtractStringRefactoring(Map<String, String> arguments)
|
||||||
throws NullPointerException {
|
throws NullPointerException {
|
||||||
|
mMode = Mode.valueOf(arguments.get("mode")); //$NON-NLS-1$
|
||||||
|
|
||||||
IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
|
if (mMode == Mode.EDIT_SOURCE) {
|
||||||
mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
|
IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
|
||||||
mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
|
mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
|
||||||
mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
|
mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
|
||||||
mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
|
mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
|
||||||
|
mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
|
||||||
|
} else {
|
||||||
|
mFile = null;
|
||||||
|
mSelectionStart = mSelectionEnd = -1;
|
||||||
|
mTokenString = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> createArgumentMap() {
|
private Map<String, String> createArgumentMap() {
|
||||||
HashMap<String, String> args = new HashMap<String, String>();
|
HashMap<String, String> args = new HashMap<String, String>();
|
||||||
args.put("file", mFile.getFullPath().toPortableString()); //$NON-NLS-1$
|
args.put("mode", mMode.name()); //$NON-NLS-1$
|
||||||
args.put("sel-start", Integer.toString(mSelectionStart)); //$NON-NLS-1$
|
if (mMode == Mode.EDIT_SOURCE) {
|
||||||
args.put("sel-end", Integer.toString(mSelectionEnd)); //$NON-NLS-1$
|
args.put("file", mFile.getFullPath().toPortableString()); //$NON-NLS-1$
|
||||||
args.put("tok-esc", mTokenString); //$NON-NLS-1$
|
args.put("sel-start", Integer.toString(mSelectionStart)); //$NON-NLS-1$
|
||||||
|
args.put("sel-end", Integer.toString(mSelectionEnd)); //$NON-NLS-1$
|
||||||
|
args.put("tok-esc", mTokenString); //$NON-NLS-1$
|
||||||
|
}
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor to use when the Extract String refactoring is called on an
|
||||||
|
* *existing* source file. Its purpose is then to get the selected string of
|
||||||
|
* the source and propose to change it by an XML id. The XML id may be a new one
|
||||||
|
* or an existing one.
|
||||||
|
*
|
||||||
|
* @param file The source file to process. Cannot be null. File must exist in workspace.
|
||||||
|
* @param selection The selection in the source file. Cannot be null or empty.
|
||||||
|
*/
|
||||||
public ExtractStringRefactoring(IFile file, ITextSelection selection) {
|
public ExtractStringRefactoring(IFile file, ITextSelection selection) {
|
||||||
|
mMode = Mode.EDIT_SOURCE;
|
||||||
mFile = file;
|
mFile = file;
|
||||||
mSelectionStart = selection.getOffset();
|
mSelectionStart = selection.getOffset();
|
||||||
mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
|
mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor to use when the Extract String refactoring is called without
|
||||||
|
* any source file. Its purpose is then to create a new XML string ID.
|
||||||
|
*
|
||||||
|
* @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be
|
||||||
|
* used.
|
||||||
|
*/
|
||||||
|
public ExtractStringRefactoring(boolean enforceNew) {
|
||||||
|
mMode = enforceNew ? Mode.MAKE_NEW_ID : Mode.MAKE_ID;
|
||||||
|
mFile = null;
|
||||||
|
mSelectionStart = mSelectionEnd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
|
* @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
|
||||||
*/
|
*/
|
||||||
@@ -226,6 +265,11 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
try {
|
try {
|
||||||
monitor.beginTask("Checking preconditions...", 5);
|
monitor.beginTask("Checking preconditions...", 5);
|
||||||
|
|
||||||
|
if (mMode != Mode.EDIT_SOURCE) {
|
||||||
|
monitor.worked(5);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
if (!checkSourceFile(mFile, status, monitor)) {
|
if (!checkSourceFile(mFile, status, monitor)) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -415,9 +459,9 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
|
|
||||||
// Prepare the change for the XML file.
|
// Prepare the change for the XML file.
|
||||||
|
|
||||||
if (!isResIdDuplicate(mTargetXmlFileWsPath, mXmlStringId)) {
|
if (!mHelper.isResIdDuplicate(mFile.getProject(), mTargetXmlFileWsPath, mXmlStringId)) {
|
||||||
// We actually change it only if the ID doesn't exist yet
|
// We actually change it only if the ID doesn't exist yet
|
||||||
Change change = createXmlChange((IFile) targetXml, mXmlStringId, mTokenString,
|
Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue,
|
||||||
status, SubMonitor.convert(monitor, 1));
|
status, SubMonitor.convert(monitor, 1));
|
||||||
if (change != null) {
|
if (change != null) {
|
||||||
mChanges.add(change);
|
mChanges.add(change);
|
||||||
@@ -428,11 +472,13 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the change to the Java compilation unit
|
if (mMode == Mode.EDIT_SOURCE) {
|
||||||
List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
|
// Prepare the change to the Java compilation unit
|
||||||
status, SubMonitor.convert(monitor, 1));
|
List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
|
||||||
if (changes != null) {
|
status, SubMonitor.convert(monitor, 1));
|
||||||
mChanges.addAll(changes);
|
if (changes != null) {
|
||||||
|
mChanges.addAll(changes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.worked(1);
|
monitor.worked(1);
|
||||||
@@ -484,6 +530,9 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
// The file exist. Attempt to parse it as a valid XML document.
|
// The file exist. Attempt to parse it as a valid XML document.
|
||||||
try {
|
try {
|
||||||
int[] indices = new int[2];
|
int[] indices = new int[2];
|
||||||
|
|
||||||
|
// TODO case where we replace the value of an existing XML String ID
|
||||||
|
|
||||||
if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$
|
if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$
|
||||||
// Indices[1] indicates whether we found > or />. It can only be 1 or 2.
|
// Indices[1] indicates whether we found > or />. It can only be 1 or 2.
|
||||||
// Indices[0] is the position of the first character of either > or />.
|
// Indices[0] is the position of the first character of either > or />.
|
||||||
@@ -865,78 +914,6 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method used by the wizard to check whether the given string ID is already
|
|
||||||
* defined in the XML file which path is given.
|
|
||||||
*
|
|
||||||
* @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
|
|
||||||
* The given file may or may not exist.
|
|
||||||
* @param stringId The string ID to find.
|
|
||||||
* @return True if such a string ID is already defined.
|
|
||||||
*/
|
|
||||||
public boolean isResIdDuplicate(String xmlFileWsPath, String stringId) {
|
|
||||||
// This is going to be called many times on the same file.
|
|
||||||
// Build a cache of the existing IDs for a given file.
|
|
||||||
if (mResIdCache == null) {
|
|
||||||
mResIdCache = new HashMap<String, HashSet<String>>();
|
|
||||||
}
|
|
||||||
HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
|
|
||||||
if (cache == null) {
|
|
||||||
cache = getResIdsForFile(xmlFileWsPath);
|
|
||||||
mResIdCache.put(xmlFileWsPath, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache.contains(stringId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract all the defined string IDs from a given file using XPath.
|
|
||||||
*
|
|
||||||
* @param xmlFileWsPath The project path of the file to parse. It may not exist.
|
|
||||||
* @return The set of all string IDs defined in the file. The returned set is always non
|
|
||||||
* null. It is empty if the file does not exist.
|
|
||||||
*/
|
|
||||||
private HashSet<String> getResIdsForFile(String xmlFileWsPath) {
|
|
||||||
HashSet<String> ids = new HashSet<String>();
|
|
||||||
|
|
||||||
if (mXPath == null) {
|
|
||||||
mXPath = AndroidXPathFactory.newXPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access the project that contains the resource that contains the compilation unit
|
|
||||||
IResource resource = getTargetXmlResource(xmlFileWsPath);
|
|
||||||
|
|
||||||
if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
|
|
||||||
InputSource source;
|
|
||||||
try {
|
|
||||||
source = new InputSource(((IFile) resource).getContents());
|
|
||||||
|
|
||||||
// We want all the IDs in an XML structure like this:
|
|
||||||
// <resources>
|
|
||||||
// <string name="ID">something</string>
|
|
||||||
// </resources>
|
|
||||||
|
|
||||||
String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$
|
|
||||||
|
|
||||||
Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
|
|
||||||
if (result instanceof NodeList) {
|
|
||||||
NodeList list = (NodeList) result;
|
|
||||||
for (int n = list.getLength() - 1; n >= 0; n--) {
|
|
||||||
String id = list.item(n).getNodeValue();
|
|
||||||
ids.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (CoreException e1) {
|
|
||||||
// IFile.getContents failed. Ignore.
|
|
||||||
} catch (XPathExpressionException e) {
|
|
||||||
// mXPath.evaluate failed. Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a file project path, returns its resource in the same project than the
|
* Given a file project path, returns its resource in the same project than the
|
||||||
* compilation unit. The resource may not exist.
|
* compilation unit. The resource may not exist.
|
||||||
@@ -950,8 +927,15 @@ class ExtractStringRefactoring extends Refactoring {
|
|||||||
/**
|
/**
|
||||||
* Sets the replacement string ID. Used by the wizard to set the user input.
|
* Sets the replacement string ID. Used by the wizard to set the user input.
|
||||||
*/
|
*/
|
||||||
public void setReplacementStringId(String replacementStringId) {
|
public void setNewStringId(String newStringId) {
|
||||||
mXmlStringId = replacementStringId;
|
mXmlStringId = newStringId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the replacement string ID. Used by the wizard to set the user input.
|
||||||
|
*/
|
||||||
|
public void setNewStringValue(String newStringValue) {
|
||||||
|
mXmlStringValue = newStringValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
|
|||||||
* @see ExtractStringInputPage
|
* @see ExtractStringInputPage
|
||||||
* @see ExtractStringRefactoring
|
* @see ExtractStringRefactoring
|
||||||
*/
|
*/
|
||||||
class ExtractStringWizard extends RefactoringWizard {
|
public class ExtractStringWizard extends RefactoringWizard {
|
||||||
|
|
||||||
private final IProject mProject;
|
private final IProject mProject;
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,13 @@
|
|||||||
package com.android.ide.eclipse.adt.ui;
|
package com.android.ide.eclipse.adt.ui;
|
||||||
|
|
||||||
import com.android.ide.eclipse.adt.AdtPlugin;
|
import com.android.ide.eclipse.adt.AdtPlugin;
|
||||||
|
import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringRefactoring;
|
||||||
|
import com.android.ide.eclipse.adt.refactorings.extractstring.ExtractStringWizard;
|
||||||
import com.android.ide.eclipse.common.resources.IResourceRepository;
|
import com.android.ide.eclipse.common.resources.IResourceRepository;
|
||||||
import com.android.ide.eclipse.common.resources.ResourceItem;
|
import com.android.ide.eclipse.common.resources.ResourceItem;
|
||||||
import com.android.ide.eclipse.common.resources.ResourceType;
|
import com.android.ide.eclipse.common.resources.ResourceType;
|
||||||
|
|
||||||
|
import org.eclipse.core.resources.IProject;
|
||||||
import org.eclipse.core.runtime.IStatus;
|
import org.eclipse.core.runtime.IStatus;
|
||||||
import org.eclipse.core.runtime.Status;
|
import org.eclipse.core.runtime.Status;
|
||||||
import org.eclipse.jface.dialogs.DialogSettings;
|
import org.eclipse.jface.dialogs.DialogSettings;
|
||||||
@@ -30,14 +33,20 @@ import org.eclipse.jface.viewers.ISelection;
|
|||||||
import org.eclipse.jface.viewers.TreePath;
|
import org.eclipse.jface.viewers.TreePath;
|
||||||
import org.eclipse.jface.viewers.TreeSelection;
|
import org.eclipse.jface.viewers.TreeSelection;
|
||||||
import org.eclipse.jface.viewers.TreeViewer;
|
import org.eclipse.jface.viewers.TreeViewer;
|
||||||
|
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
|
||||||
|
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
|
||||||
import org.eclipse.swt.SWT;
|
import org.eclipse.swt.SWT;
|
||||||
|
import org.eclipse.swt.events.SelectionAdapter;
|
||||||
import org.eclipse.swt.events.SelectionEvent;
|
import org.eclipse.swt.events.SelectionEvent;
|
||||||
import org.eclipse.swt.events.SelectionListener;
|
import org.eclipse.swt.events.SelectionListener;
|
||||||
import org.eclipse.swt.layout.GridData;
|
import org.eclipse.swt.layout.GridData;
|
||||||
|
import org.eclipse.swt.widgets.Button;
|
||||||
import org.eclipse.swt.widgets.Composite;
|
import org.eclipse.swt.widgets.Composite;
|
||||||
import org.eclipse.swt.widgets.Control;
|
import org.eclipse.swt.widgets.Control;
|
||||||
import org.eclipse.swt.widgets.Shell;
|
import org.eclipse.swt.widgets.Shell;
|
||||||
import org.eclipse.swt.widgets.Tree;
|
import org.eclipse.swt.widgets.Tree;
|
||||||
|
import org.eclipse.ui.IWorkbench;
|
||||||
|
import org.eclipse.ui.PlatformUI;
|
||||||
import org.eclipse.ui.dialogs.FilteredTree;
|
import org.eclipse.ui.dialogs.FilteredTree;
|
||||||
import org.eclipse.ui.dialogs.PatternFilter;
|
import org.eclipse.ui.dialogs.PatternFilter;
|
||||||
import org.eclipse.ui.dialogs.SelectionStatusDialog;
|
import org.eclipse.ui.dialogs.SelectionStatusDialog;
|
||||||
@@ -58,21 +67,24 @@ public class ReferenceChooserDialog extends SelectionStatusDialog {
|
|||||||
|
|
||||||
private IResourceRepository mResources;
|
private IResourceRepository mResources;
|
||||||
private String mCurrentResource;
|
private String mCurrentResource;
|
||||||
|
|
||||||
private FilteredTree mFilteredTree;
|
private FilteredTree mFilteredTree;
|
||||||
|
private Button mNewResButton;
|
||||||
|
private final IProject mProject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param project
|
||||||
* @param parent
|
* @param parent
|
||||||
*/
|
*/
|
||||||
public ReferenceChooserDialog(IResourceRepository resources, Shell parent) {
|
public ReferenceChooserDialog(IProject project, IResourceRepository resources, Shell parent) {
|
||||||
super(parent);
|
super(parent);
|
||||||
|
mProject = project;
|
||||||
|
mResources = resources;
|
||||||
|
|
||||||
int shellStyle = getShellStyle();
|
int shellStyle = getShellStyle();
|
||||||
setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
|
setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
|
||||||
|
|
||||||
setTitle("Reference Dialog");
|
setTitle("Reference Chooser");
|
||||||
setMessage(String.format("Choose a resource"));
|
setMessage(String.format("Choose a resource"));
|
||||||
mResources = resources;
|
|
||||||
|
|
||||||
setDialogBoundsSettings(sDialogSettings, getDialogBoundsStrategy());
|
setDialogBoundsSettings(sDialogSettings, getDialogBoundsStrategy());
|
||||||
}
|
}
|
||||||
@@ -117,9 +129,22 @@ public class ReferenceChooserDialog extends SelectionStatusDialog {
|
|||||||
// setup the initial selection
|
// setup the initial selection
|
||||||
setupInitialSelection();
|
setupInitialSelection();
|
||||||
|
|
||||||
|
// create the "New Resource" button
|
||||||
|
createNewResButtons(top);
|
||||||
|
|
||||||
return top;
|
return top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the "New Resource" button.
|
||||||
|
* @param top the parent composite
|
||||||
|
*/
|
||||||
|
private void createNewResButtons(Composite top) {
|
||||||
|
mNewResButton = new Button(top, SWT.NONE);
|
||||||
|
mNewResButton.addSelectionListener(new OnNewResButtonSelected());
|
||||||
|
updateNewResButton();
|
||||||
|
}
|
||||||
|
|
||||||
private void createFilteredTree(Composite parent) {
|
private void createFilteredTree(Composite parent) {
|
||||||
mFilteredTree = new FilteredTree(parent, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION,
|
mFilteredTree = new FilteredTree(parent, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION,
|
||||||
new PatternFilter());
|
new PatternFilter());
|
||||||
@@ -154,6 +179,7 @@ public class ReferenceChooserDialog extends SelectionStatusDialog {
|
|||||||
|
|
||||||
protected void handleSelection() {
|
protected void handleSelection() {
|
||||||
validateCurrentSelection();
|
validateCurrentSelection();
|
||||||
|
updateNewResButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleDoubleClick() {
|
protected void handleDoubleClick() {
|
||||||
@@ -206,6 +232,65 @@ public class ReferenceChooserDialog extends SelectionStatusDialog {
|
|||||||
return status.isOK();
|
return status.isOK();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the new res button when the list selection changes.
|
||||||
|
* The name of the button changes depending on the resource.
|
||||||
|
*/
|
||||||
|
private void updateNewResButton() {
|
||||||
|
ResourceType type = getSelectedResourceType();
|
||||||
|
|
||||||
|
// We only support adding new strings right now
|
||||||
|
mNewResButton.setEnabled(type == ResourceType.STRING);
|
||||||
|
|
||||||
|
String title = String.format("New %1$s", type == null ? "Resource" : type.getDisplayName());
|
||||||
|
mNewResButton.setText(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when the mNewResButton is selected by the user.
|
||||||
|
*/
|
||||||
|
private class OnNewResButtonSelected extends SelectionAdapter {
|
||||||
|
@Override
|
||||||
|
public void widgetSelected(SelectionEvent e) {
|
||||||
|
super.widgetSelected(e);
|
||||||
|
|
||||||
|
ResourceType type = getSelectedResourceType();
|
||||||
|
|
||||||
|
// We currently only support strings
|
||||||
|
if (type == ResourceType.STRING) {
|
||||||
|
|
||||||
|
ExtractStringRefactoring ref = new ExtractStringRefactoring(true /*enforceNew*/);
|
||||||
|
RefactoringWizard wizard = new ExtractStringWizard(ref, mProject);
|
||||||
|
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
|
||||||
|
try {
|
||||||
|
IWorkbench w = PlatformUI.getWorkbench();
|
||||||
|
op.run(w.getDisplay().getActiveShell(), wizard.getDefaultPageTitle());
|
||||||
|
|
||||||
|
// TODO Select string
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
// Interrupted. Pass.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ResourceType} of the selected element, if any.
|
||||||
|
* Returns null if nothing suitable is selected.
|
||||||
|
*/
|
||||||
|
private ResourceType getSelectedResourceType() {
|
||||||
|
ResourceType type = null;
|
||||||
|
|
||||||
|
TreePath selection = getSelection();
|
||||||
|
if (selection != null && selection.getSegmentCount() > 0) {
|
||||||
|
Object first = selection.getFirstSegment();
|
||||||
|
if (first instanceof ResourceType) {
|
||||||
|
type = (ResourceType) first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the initial selection.
|
* Sets up the initial selection.
|
||||||
* <p/>
|
* <p/>
|
||||||
|
|||||||
@@ -0,0 +1,439 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 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.wizards.newstring;
|
||||||
|
|
||||||
|
|
||||||
|
import com.android.ide.eclipse.adt.ui.ConfigurationSelector;
|
||||||
|
import com.android.ide.eclipse.common.AndroidConstants;
|
||||||
|
import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
|
||||||
|
import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
|
||||||
|
import com.android.sdklib.SdkConstants;
|
||||||
|
|
||||||
|
import org.eclipse.core.resources.IFolder;
|
||||||
|
import org.eclipse.core.resources.IProject;
|
||||||
|
import org.eclipse.core.resources.IResource;
|
||||||
|
import org.eclipse.core.runtime.CoreException;
|
||||||
|
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.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.Group;
|
||||||
|
import org.eclipse.swt.widgets.Label;
|
||||||
|
import org.eclipse.swt.widgets.Text;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class NewStringBaseImpl {
|
||||||
|
|
||||||
|
public interface INewStringPageCallback {
|
||||||
|
/**
|
||||||
|
* Creates the top group with the field to replace which string and by what
|
||||||
|
* and by which options.
|
||||||
|
*
|
||||||
|
* @param content A composite with a 1-column grid layout
|
||||||
|
* @return The {@link Text} field for the new String ID name.
|
||||||
|
*/
|
||||||
|
public Text createStringGroup(Composite content);
|
||||||
|
|
||||||
|
/** Implements {@link WizardPage#setErrorMessage(String)} */
|
||||||
|
public void setErrorMessage(String newMessage);
|
||||||
|
/** Implements {@link WizardPage#setMessage(String, int)} */
|
||||||
|
public void setMessage(String msg, int type);
|
||||||
|
/** Implements {@link WizardPage#setPageComplete(boolean)} */
|
||||||
|
public void setPageComplete(boolean success);
|
||||||
|
|
||||||
|
public void postValidatePage(ValidationStatus status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidationStatus {
|
||||||
|
private String mError = null;
|
||||||
|
private String mMessage = null;
|
||||||
|
public int mMessageType = WizardPage.NONE;
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return getError() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setError(String error) {
|
||||||
|
mError = error;
|
||||||
|
mMessageType = WizardPage.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getError() {
|
||||||
|
return mError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String msg, int type) {
|
||||||
|
mMessage = msg;
|
||||||
|
mMessageType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return mMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMessageType() {
|
||||||
|
return mMessageType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last res file path used, shared across the session instances but specific to the
|
||||||
|
* current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */
|
||||||
|
private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>();
|
||||||
|
|
||||||
|
/** The project where the user selection happened. */
|
||||||
|
private final IProject mProject;
|
||||||
|
/** Text field where the user enters the new ID. */
|
||||||
|
private Text mStringIdField;
|
||||||
|
/** The configuration selector, to select the resource path of the XML file. */
|
||||||
|
private ConfigurationSelector mConfigSelector;
|
||||||
|
/** The combo to display the existing XML files or enter a new one. */
|
||||||
|
private Combo mResFileCombo;
|
||||||
|
|
||||||
|
private NewStringHelper mHelper = new NewStringHelper();
|
||||||
|
|
||||||
|
/** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
|
||||||
|
* a leaf file name ending with .xml */
|
||||||
|
private static final Pattern RES_XML_FILE_REGEX = Pattern.compile(
|
||||||
|
"/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$
|
||||||
|
/** 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 static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private final INewStringPageCallback mWizardPage;
|
||||||
|
|
||||||
|
public NewStringBaseImpl(IProject project, INewStringPageCallback wizardPage) {
|
||||||
|
mProject = project;
|
||||||
|
mWizardPage = wizardPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the UI for the new string wizard.
|
||||||
|
*/
|
||||||
|
public void createControl(Composite parent) {
|
||||||
|
mStringIdField = mWizardPage.createStringGroup(parent);
|
||||||
|
createResFileGroup(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the lower group with the fields to choose the resource confirmation and
|
||||||
|
* the target XML file.
|
||||||
|
*
|
||||||
|
* @param content A composite with a 1-column grid layout
|
||||||
|
*/
|
||||||
|
private void createResFileGroup(Composite content) {
|
||||||
|
|
||||||
|
Group group = new Group(content, SWT.NONE);
|
||||||
|
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
|
group.setText("XML resource to edit");
|
||||||
|
|
||||||
|
GridLayout layout = new GridLayout();
|
||||||
|
layout.numColumns = 2;
|
||||||
|
group.setLayout(layout);
|
||||||
|
|
||||||
|
// line: selection of the res config
|
||||||
|
|
||||||
|
Label label;
|
||||||
|
label = new Label(group, SWT.NONE);
|
||||||
|
label.setText("Configuration:");
|
||||||
|
|
||||||
|
mConfigSelector = new ConfigurationSelector(group);
|
||||||
|
GridData gd = new GridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
|
||||||
|
gd.widthHint = ConfigurationSelector.WIDTH_HINT;
|
||||||
|
gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
|
||||||
|
mConfigSelector.setLayoutData(gd);
|
||||||
|
OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
|
||||||
|
mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
|
||||||
|
|
||||||
|
// line: selection of the output file
|
||||||
|
|
||||||
|
label = new Label(group, SWT.NONE);
|
||||||
|
label.setText("Resource file:");
|
||||||
|
|
||||||
|
mResFileCombo = new Combo(group, SWT.DROP_DOWN);
|
||||||
|
mResFileCombo.select(0);
|
||||||
|
mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
|
mResFileCombo.addModifyListener(onConfigSelectorUpdated);
|
||||||
|
|
||||||
|
// set output file name to the last one used
|
||||||
|
|
||||||
|
String projPath = mProject.getFullPath().toPortableString();
|
||||||
|
String filePath = sLastResFilePath.get(projPath);
|
||||||
|
|
||||||
|
mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
|
||||||
|
onConfigSelectorUpdated.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates fields of the wizard input page. Displays errors as appropriate and
|
||||||
|
* enable the "Next" button (or not) by calling
|
||||||
|
* {@link INewStringPageCallback#setPageComplete(boolean)}.
|
||||||
|
*
|
||||||
|
* @return True if the page has been positively validated. It may still have warnings.
|
||||||
|
*/
|
||||||
|
public boolean validatePage() {
|
||||||
|
ValidationStatus status = new ValidationStatus();
|
||||||
|
|
||||||
|
validateStringFields(status);
|
||||||
|
if (status.success()) {
|
||||||
|
validatePathFields(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
mWizardPage.postValidatePage(status);
|
||||||
|
|
||||||
|
mWizardPage.setErrorMessage(status.getError());
|
||||||
|
mWizardPage.setMessage(status.getMessage(), status.getMessageType());
|
||||||
|
mWizardPage.setPageComplete(status.success());
|
||||||
|
return status.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateStringFields(ValidationStatus status) {
|
||||||
|
|
||||||
|
String text = mStringIdField.getText().trim();
|
||||||
|
if (text == null || text.length() < 1) {
|
||||||
|
status.setError("Please provide a resource ID to replace with.");
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
boolean ok = i == 0 ?
|
||||||
|
Character.isJavaIdentifierStart(c) :
|
||||||
|
Character.isJavaIdentifierPart(c);
|
||||||
|
if (!ok) {
|
||||||
|
status.setError(String.format(
|
||||||
|
"The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.",
|
||||||
|
c, i+1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationStatus validatePathFields(ValidationStatus status) {
|
||||||
|
String resFile = getResFileProjPath();
|
||||||
|
if (resFile == null || resFile.length() == 0) {
|
||||||
|
status.setError("A resource file name is required.");
|
||||||
|
} else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) {
|
||||||
|
status.setError("The XML file name is not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.success()) {
|
||||||
|
sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
|
||||||
|
|
||||||
|
String text = mStringIdField.getText().trim();
|
||||||
|
|
||||||
|
if (mHelper.isResIdDuplicate(mProject, resFile, text)) {
|
||||||
|
status.setMessage(
|
||||||
|
String.format("There's already a string item called '%1$s' in %2$s.",
|
||||||
|
text, resFile), WizardPage.WARNING);
|
||||||
|
} else if (mProject.findMember(resFile) == null) {
|
||||||
|
status.setMessage(
|
||||||
|
String.format("File %2$s does not exist and will be created.",
|
||||||
|
text, resFile), WizardPage.INFORMATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResFileProjPath() {
|
||||||
|
return mResFileCombo.getText().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
|
||||||
|
|
||||||
|
/** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
|
||||||
|
private final Pattern mPathRegex = Pattern.compile(
|
||||||
|
"(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** Temporary config object used to retrieve the Config Selector value. */
|
||||||
|
private FolderConfiguration mTempConfig = new FolderConfiguration();
|
||||||
|
|
||||||
|
private HashMap<String, TreeSet<String>> mFolderCache =
|
||||||
|
new HashMap<String, TreeSet<String>>();
|
||||||
|
private String mLastFolderUsedInCombo = null;
|
||||||
|
private boolean mInternalConfigChange;
|
||||||
|
private boolean mInternalFileComboChange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when the {@link ConfigurationSelector} has been changed.
|
||||||
|
* <p/>
|
||||||
|
* The callback does the following:
|
||||||
|
* <ul>
|
||||||
|
* <li> Examine the current file name to retrieve the XML filename, if any.
|
||||||
|
* <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/).
|
||||||
|
* <li> Examine the path to retrieve all the files in it. Keep those in a local cache.
|
||||||
|
* <li> If the XML filename from step 1 is not in the file list, it's a custom file name.
|
||||||
|
* Insert it and sort it.
|
||||||
|
* <li> Re-populate the file combo with all the choices.
|
||||||
|
* <li> Select the original XML file.
|
||||||
|
*/
|
||||||
|
public void run() {
|
||||||
|
if (mInternalConfigChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current leafname, if any
|
||||||
|
String leafName = ""; //$NON-NLS-1$
|
||||||
|
String currPath = mResFileCombo.getText();
|
||||||
|
Matcher m = mPathRegex.matcher(currPath);
|
||||||
|
if (m.matches()) {
|
||||||
|
// Note: groups 1 and 2 cannot be null.
|
||||||
|
leafName = m.group(2);
|
||||||
|
currPath = m.group(1);
|
||||||
|
} else {
|
||||||
|
// There was a path but it was invalid. Ignore it.
|
||||||
|
currPath = ""; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate the res path from the current configuration
|
||||||
|
mConfigSelector.getConfiguration(mTempConfig);
|
||||||
|
StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
|
||||||
|
sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
|
||||||
|
sb.append('/');
|
||||||
|
|
||||||
|
String newPath = sb.toString();
|
||||||
|
if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
|
||||||
|
// Path has not changed. No need to reload.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the files at the new path
|
||||||
|
|
||||||
|
TreeSet<String> filePaths = mFolderCache.get(newPath);
|
||||||
|
|
||||||
|
if (filePaths == null) {
|
||||||
|
filePaths = new TreeSet<String>();
|
||||||
|
|
||||||
|
IFolder folder = mProject.getFolder(newPath);
|
||||||
|
if (folder != null && folder.exists()) {
|
||||||
|
try {
|
||||||
|
for (IResource res : folder.members()) {
|
||||||
|
String name = res.getName();
|
||||||
|
if (res.getType() == IResource.FILE && name.endsWith(".xml")) { //$NON-NLS-1$
|
||||||
|
filePaths.add(newPath + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (CoreException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mFolderCache.put(newPath, filePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
currPath = newPath + leafName;
|
||||||
|
if (leafName.length() > 0 && !filePaths.contains(currPath)) {
|
||||||
|
filePaths.add(currPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the combo
|
||||||
|
try {
|
||||||
|
mInternalFileComboChange = true;
|
||||||
|
|
||||||
|
mResFileCombo.removeAll();
|
||||||
|
|
||||||
|
for (String filePath : filePaths) {
|
||||||
|
mResFileCombo.add(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = -1;
|
||||||
|
if (leafName.length() > 0) {
|
||||||
|
index = mResFileCombo.indexOf(currPath);
|
||||||
|
if (index >= 0) {
|
||||||
|
mResFileCombo.select(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
mResFileCombo.setText(currPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
mLastFolderUsedInCombo = newPath;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
mInternalFileComboChange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally validate the whole page
|
||||||
|
validatePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when {@link NewStringBaseImpl#mResFileCombo} has been
|
||||||
|
* modified.
|
||||||
|
*/
|
||||||
|
public void modifyText(ModifyEvent e) {
|
||||||
|
if (mInternalFileComboChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String wsFolderPath = mResFileCombo.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$
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
mInternalFileComboChange = true;
|
||||||
|
mResFileCombo.setText(wsFolderPath);
|
||||||
|
mInternalFileComboChange = 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];
|
||||||
|
|
||||||
|
if (folderName != null && !folderName.equals(wsFolderPath)) {
|
||||||
|
// update config selector
|
||||||
|
mInternalConfigChange = true;
|
||||||
|
mConfigSelector.setConfiguration(folderSegments);
|
||||||
|
mInternalConfigChange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 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.wizards.newstring;
|
||||||
|
|
||||||
|
import com.android.ide.eclipse.common.project.AndroidXPathFactory;
|
||||||
|
|
||||||
|
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.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import javax.xml.xpath.XPath;
|
||||||
|
import javax.xml.xpath.XPathConstants;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class NewStringHelper {
|
||||||
|
|
||||||
|
/** A temporary cache of R.string IDs defined by a given xml file. The key is the
|
||||||
|
* project path of the file, the data is a set of known string Ids for that file. */
|
||||||
|
private HashMap<String,HashSet<String>> mResIdCache;
|
||||||
|
/** An instance of XPath, created lazily on demand. */
|
||||||
|
private XPath mXPath;
|
||||||
|
|
||||||
|
public NewStringHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method used by the wizard to check whether the given string ID is already
|
||||||
|
* defined in the XML file which path is given.
|
||||||
|
*
|
||||||
|
* @param project The project contain the XML file.
|
||||||
|
* @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
|
||||||
|
* The given file may or may not exist.
|
||||||
|
* @param stringId The string ID to find.
|
||||||
|
* @return True if such a string ID is already defined.
|
||||||
|
*/
|
||||||
|
public boolean isResIdDuplicate(IProject project, String xmlFileWsPath, String stringId) {
|
||||||
|
// This is going to be called many times on the same file.
|
||||||
|
// Build a cache of the existing IDs for a given file.
|
||||||
|
if (mResIdCache == null) {
|
||||||
|
mResIdCache = new HashMap<String, HashSet<String>>();
|
||||||
|
}
|
||||||
|
HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
|
||||||
|
if (cache == null) {
|
||||||
|
cache = getResIdsForFile(project, xmlFileWsPath);
|
||||||
|
mResIdCache.put(xmlFileWsPath, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.contains(stringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all the defined string IDs from a given file using XPath.
|
||||||
|
* @param project The project contain the XML file.
|
||||||
|
* @param xmlFileWsPath The project path of the file to parse. It may not exist.
|
||||||
|
* @return The set of all string IDs defined in the file. The returned set is always non
|
||||||
|
* null. It is empty if the file does not exist.
|
||||||
|
*/
|
||||||
|
private HashSet<String> getResIdsForFile(IProject project, String xmlFileWsPath) {
|
||||||
|
HashSet<String> ids = new HashSet<String>();
|
||||||
|
|
||||||
|
if (mXPath == null) {
|
||||||
|
mXPath = AndroidXPathFactory.newXPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access the project that contains the resource that contains the compilation unit
|
||||||
|
IResource resource = project.getFile(xmlFileWsPath);
|
||||||
|
|
||||||
|
if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
|
||||||
|
InputSource source;
|
||||||
|
try {
|
||||||
|
source = new InputSource(((IFile) resource).getContents());
|
||||||
|
|
||||||
|
// We want all the IDs in an XML structure like this:
|
||||||
|
// <resources>
|
||||||
|
// <string name="ID">something</string>
|
||||||
|
// </resources>
|
||||||
|
|
||||||
|
String xpathExpr = "/resources/string/@name"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
|
||||||
|
if (result instanceof NodeList) {
|
||||||
|
NodeList list = (NodeList) result;
|
||||||
|
for (int n = list.getLength() - 1; n >= 0; n--) {
|
||||||
|
String id = list.item(n).getNodeValue();
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (CoreException e1) {
|
||||||
|
// IFile.getContents failed. Ignore.
|
||||||
|
} catch (XPathExpressionException e) {
|
||||||
|
// mXPath.evaluate failed. Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 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.wizards.newstring;
|
||||||
|
|
||||||
|
import org.eclipse.core.resources.IProject;
|
||||||
|
import org.eclipse.jface.wizard.Wizard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class NewStringWizard extends Wizard {
|
||||||
|
|
||||||
|
protected static final String MAIN_PAGE_NAME = "newXmlStringPage"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private NewStringWizardPage mMainPage;
|
||||||
|
|
||||||
|
public NewStringWizard(IProject project) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
mMainPage = createMainPage(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the wizard page.
|
||||||
|
* <p/>
|
||||||
|
* Please do NOT override this method.
|
||||||
|
* <p/>
|
||||||
|
* This is protected so that it can be overridden by unit tests.
|
||||||
|
* However the contract of this class is private and NO ATTEMPT will be made
|
||||||
|
* to maintain compatibility between different versions of the plugin.
|
||||||
|
* @param project
|
||||||
|
*/
|
||||||
|
protected NewStringWizardPage createMainPage(IProject project) {
|
||||||
|
return new NewStringWizardPage(project, MAIN_PAGE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addPages() {
|
||||||
|
addPage(mMainPage);
|
||||||
|
super.addPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see org.eclipse.jface.wizard.Wizard#performFinish()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean performFinish() {
|
||||||
|
// pass
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 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.wizards.newstring;
|
||||||
|
|
||||||
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl.INewStringPageCallback;
|
||||||
|
import com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl.ValidationStatus;
|
||||||
|
|
||||||
|
import org.eclipse.core.resources.IProject;
|
||||||
|
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.layout.GridData;
|
||||||
|
import org.eclipse.swt.layout.GridLayout;
|
||||||
|
import org.eclipse.swt.widgets.Composite;
|
||||||
|
import org.eclipse.swt.widgets.Group;
|
||||||
|
import org.eclipse.swt.widgets.Label;
|
||||||
|
import org.eclipse.swt.widgets.Text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class NewStringWizardPage extends WizardPage implements INewStringPageCallback {
|
||||||
|
|
||||||
|
private NewStringBaseImpl mImpl;
|
||||||
|
|
||||||
|
/** Field displaying the user-selected string to be replaced. */
|
||||||
|
private Label mStringValueField;
|
||||||
|
|
||||||
|
private String mNewStringId;
|
||||||
|
|
||||||
|
public NewStringWizardPage(IProject project, String pageName) {
|
||||||
|
super(pageName);
|
||||||
|
mImpl = new NewStringBaseImpl(project, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewStringValue() {
|
||||||
|
return mStringValueField.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewStringId() {
|
||||||
|
return mNewStringId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResFilePathProjPath() {
|
||||||
|
return mImpl.getResFileProjPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the UI for the new string wizard.
|
||||||
|
*/
|
||||||
|
public void createControl(Composite parent) {
|
||||||
|
Composite content = new Composite(parent, SWT.NONE);
|
||||||
|
GridLayout layout = new GridLayout();
|
||||||
|
layout.numColumns = 1;
|
||||||
|
content.setLayout(layout);
|
||||||
|
|
||||||
|
mImpl.createControl(content);
|
||||||
|
setControl(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the top group with the field to replace which string and by what
|
||||||
|
* and by which options.
|
||||||
|
*
|
||||||
|
* @param content A composite with a 1-column grid layout
|
||||||
|
* @return The {@link Text} field for the new String ID name.
|
||||||
|
*/
|
||||||
|
public Text createStringGroup(Composite content) {
|
||||||
|
|
||||||
|
Group group = new Group(content, SWT.NONE);
|
||||||
|
group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
|
group.setText("New String");
|
||||||
|
|
||||||
|
GridLayout layout = new GridLayout();
|
||||||
|
layout.numColumns = 2;
|
||||||
|
group.setLayout(layout);
|
||||||
|
|
||||||
|
Label label = new Label(group, SWT.NONE);
|
||||||
|
label.setText("String:");
|
||||||
|
|
||||||
|
mStringValueField = new Label(group, SWT.NONE);
|
||||||
|
mStringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
|
mStringValueField.setText(""); //$NON-NLS-1$
|
||||||
|
|
||||||
|
// TODO provide an option to refactor all known occurences of this string.
|
||||||
|
|
||||||
|
// line : Textfield for new ID
|
||||||
|
|
||||||
|
label = new Label(group, SWT.NONE);
|
||||||
|
label.setText("Replace by R.string.");
|
||||||
|
|
||||||
|
final Text stringIdField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
|
||||||
|
stringIdField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
|
||||||
|
stringIdField.setText("");
|
||||||
|
|
||||||
|
mNewStringId = stringIdField.getText().trim();
|
||||||
|
|
||||||
|
stringIdField.addModifyListener(new ModifyListener() {
|
||||||
|
public void modifyText(ModifyEvent e) {
|
||||||
|
if (mImpl.validatePage()) {
|
||||||
|
mNewStringId = stringIdField.getText().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return stringIdField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void postValidatePage(ValidationStatus status) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,7 +144,9 @@ public class UiResourceAttributeNode extends UiTextAttributeNode {
|
|||||||
return dlg.getCurrentResource();
|
return dlg.getCurrentResource();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ReferenceChooserDialog dlg = new ReferenceChooserDialog(projectRepository,
|
ReferenceChooserDialog dlg = new ReferenceChooserDialog(
|
||||||
|
project,
|
||||||
|
projectRepository,
|
||||||
shell);
|
shell);
|
||||||
|
|
||||||
dlg.setCurrentResource(currentValue);
|
dlg.setCurrentResource(currentValue);
|
||||||
|
|||||||
Reference in New Issue
Block a user