package com.android.ide.eclipse.adt.refactorings.extractstring;
-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 com.android.ide.eclipse.adt.wizards.newstring.NewStringBaseImpl;
+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.IFolder;
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.WizardPage;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
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;
-
/**
* @see ExtractStringRefactoring
*/
-class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage {
-
- /** 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;
-
- /** 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;
+class ExtractStringInputPage extends UserInputWizardPage
+ implements IWizardPage, INewStringPageCallback {
- /** 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";
+ private NewStringBaseImpl mImpl;
public ExtractStringInputPage(IProject project) {
super("ExtractStringInputPage"); //$NON-NLS-1$
- mProject = project;
+ mImpl = new NewStringBaseImpl(project, this);
}
/**
* {@link ExtractStringRefactoring}.
*/
public void createControl(Composite parent) {
-
Composite content = new Composite(parent, SWT.NONE);
-
GridLayout layout = new GridLayout();
layout.numColumns = 1;
content.setLayout(layout);
-
- createStringReplacementGroup(content);
- createResFileGroup(content);
- validatePage();
+ mImpl.createControl(content);
setControl(content);
}
* 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.
*/
- private void createStringReplacementGroup(Composite content) {
+ public Text createStringGroup(Composite content) {
final ExtractStringRefactoring ref = getOurRefactoring();
layout.numColumns = 2;
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.setText("String:");
String selectedString = ref.getTokenString();
- mStringLabel = new Label(group, SWT.NONE);
- mStringLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
- mStringLabel.setText(selectedString != null ? selectedString : "");
+ final Text stringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ stringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ 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
// just the one.
label = new Label(group, SWT.NONE);
label.setText("Replace by R.string.");
- mNewIdTextField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
- mNewIdTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
- mNewIdTextField.setText(guessId(selectedString));
+ final Text stringIdField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ stringIdField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ 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) {
- if (validatePage()) {
- ref.setReplacementStringId(mNewIdTextField.getText().trim());
+ if (mImpl.validatePage()) {
+ ref.setNewStringId(stringIdField.getText().trim());
}
}
});
- }
-
- /**
- * 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();
+ return stringIdField;
}
/**
* Utility method to guess a suitable new XML ID based on the selected string.
*/
private String guessId(String text) {
+ if (text == null) {
+ return ""; //$NON-NLS-1$
+ }
+
// make lower case
text = text.toLowerCase();
return (ExtractStringRefactoring) getRefactoring();
}
- /**
- * Validates fields of the wizard input page. Displays errors as appropriate and
- * enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}.
- *
- * @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 void postValidatePage(ValidationStatus status) {
+ ExtractStringRefactoring ref = getOurRefactoring();
+ ref.setTargetFile(mImpl.getResFileProjPath());
}
-
- 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();
- }
- }
-
}
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.project.AndroidManifestParser;
-import com.android.ide.eclipse.common.project.AndroidXPathFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
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
* such as R.string.foo.
* <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
* 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
- * in the target XML file.
+ * {@link NewStringHelper#isResIdDuplicate(IProject, String, String)} to check whether the new
+ * ID is already defined in the target XML file.
* <li> Once Preview or Finish is selected in the wizard, the
* {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input
* and compute the actual changes.
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
* </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;
- /** 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;
- /** 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;
/** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */
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;
/** The XML string ID selected by the user in the wizard. */
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
* in the wizard. */
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
* used by {@link #createChange(IProgressMonitor)}. */
private ArrayList<Change> mChanges;
+
+ private NewStringHelper mHelper = new NewStringHelper();
public ExtractStringRefactoring(Map<String, String> arguments)
throws NullPointerException {
-
- IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
- mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
- mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$NON-NLS-1$
- mSelectionEnd = Integer.parseInt(arguments.get("sel-end")); //$NON-NLS-1$
- mTokenString = arguments.get("tok-esc"); //$NON-NLS-1$
+ mMode = Mode.valueOf(arguments.get("mode")); //$NON-NLS-1$
+
+ if (mMode == Mode.EDIT_SOURCE) {
+ IPath path = Path.fromPortableString(arguments.get("file")); //$NON-NLS-1$
+ mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ mSelectionStart = Integer.parseInt(arguments.get("sel-start")); //$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() {
HashMap<String, String> args = new HashMap<String, String>();
- args.put("file", mFile.getFullPath().toPortableString()); //$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$
+ args.put("mode", mMode.name()); //$NON-NLS-1$
+ if (mMode == Mode.EDIT_SOURCE) {
+ args.put("file", mFile.getFullPath().toPortableString()); //$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;
}
+ /**
+ * 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) {
+ mMode = Mode.EDIT_SOURCE;
mFile = file;
mSelectionStart = selection.getOffset();
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()
*/
@Override
try {
monitor.beginTask("Checking preconditions...", 5);
+
+ if (mMode != Mode.EDIT_SOURCE) {
+ monitor.worked(5);
+ return status;
+ }
if (!checkSourceFile(mFile, status, monitor)) {
return status;
status.addFatalError("Missing target xml file path");
}
monitor.worked(1);
-
+
// Either that resource must not exist or it must be a writeable file.
IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath);
if (targetXml != null) {
// 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
- Change change = createXmlChange((IFile) targetXml, mXmlStringId, mTokenString,
+ Change change = createXmlChange((IFile) targetXml, mXmlStringId, mXmlStringValue,
status, SubMonitor.convert(monitor, 1));
if (change != null) {
mChanges.add(change);
if (status.hasError()) {
return status;
}
-
- // Prepare the change to the Java compilation unit
- List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
- status, SubMonitor.convert(monitor, 1));
- if (changes != null) {
- mChanges.addAll(changes);
+
+ if (mMode == Mode.EDIT_SOURCE) {
+ // Prepare the change to the Java compilation unit
+ List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
+ status, SubMonitor.convert(monitor, 1));
+ if (changes != null) {
+ mChanges.addAll(changes);
+ }
}
monitor.worked(1);
// The file exist. Attempt to parse it as a valid XML document.
try {
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$
// 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 />.
}
/**
- * 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
* compilation unit. The resource may not exist.
*/
/**
* Sets the replacement string ID. Used by the wizard to set the user input.
*/
- public void setReplacementStringId(String replacementStringId) {
- mXmlStringId = replacementStringId;
+ public void setNewStringId(String newStringId) {
+ mXmlStringId = newStringId;
+ }
+
+ /**
+ * Sets the replacement string ID. Used by the wizard to set the user input.
+ */
+ public void setNewStringValue(String newStringValue) {
+ mXmlStringValue = newStringValue;
}
/**
* @see ExtractStringInputPage
* @see ExtractStringRefactoring
*/
-class ExtractStringWizard extends RefactoringWizard {
+public class ExtractStringWizard extends RefactoringWizard {
private final IProject mProject;
package com.android.ide.eclipse.adt.ui;
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.ResourceItem;
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.Status;
import org.eclipse.jface.dialogs.DialogSettings;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
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.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
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.PatternFilter;
import org.eclipse.ui.dialogs.SelectionStatusDialog;
private IResourceRepository mResources;
private String mCurrentResource;
-
private FilteredTree mFilteredTree;
+ private Button mNewResButton;
+ private final IProject mProject;
/**
+ * @param project
* @param parent
*/
- public ReferenceChooserDialog(IResourceRepository resources, Shell parent) {
+ public ReferenceChooserDialog(IProject project, IResourceRepository resources, Shell parent) {
super(parent);
+ mProject = project;
+ mResources = resources;
int shellStyle = getShellStyle();
setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
- setTitle("Reference Dialog");
+ setTitle("Reference Chooser");
setMessage(String.format("Choose a resource"));
- mResources = resources;
setDialogBoundsSettings(sDialogSettings, getDialogBoundsStrategy());
}
// create the filtered tree
createFilteredTree(top);
-
+
// setup the initial selection
setupInitialSelection();
+ // create the "New Resource" button
+ createNewResButtons(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) {
mFilteredTree = new FilteredTree(parent, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION,
new PatternFilter());
protected void handleSelection() {
validateCurrentSelection();
+ updateNewResButton();
}
protected void handleDoubleClick() {
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.
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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
+ }
+}
return dlg.getCurrentResource();
}
} else {
- ReferenceChooserDialog dlg = new ReferenceChooserDialog(projectRepository,
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(
+ project,
+ projectRepository,
shell);
dlg.setCurrentResource(currentValue);