From b6ae62d621b3ce744727fb4cac7e6ae79b524657 Mon Sep 17 00:00:00 2001 From: Raphael Moll <> Date: Thu, 2 Apr 2009 13:42:29 -0700 Subject: [PATCH] AI 144283: ADT: Enhance Resource Chooser with ability to create new XML strings. That's a first pass. There's a fair bit of refactoring involved, so it's split in two CLs. Next CL will add more functionality. BUG=1722971 Automated import of CL 144283 --- .../extractstring/ExtractStringInputPage.java | 393 ++---------------- .../extractstring/ExtractStringRefactoring.java | 210 +++++----- .../extractstring/ExtractStringWizard.java | 2 +- .../ide/eclipse/adt/ui/ReferenceChooserDialog.java | 95 ++++- .../adt/wizards/newstring/NewStringBaseImpl.java | 439 +++++++++++++++++++++ .../adt/wizards/newstring/NewStringHelper.java | 122 ++++++ .../adt/wizards/newstring/NewStringWizard.java | 66 ++++ .../adt/wizards/newstring/NewStringWizardPage.java | 127 ++++++ .../editors/uimodel/UiResourceAttributeNode.java | 4 +- 9 files changed, 985 insertions(+), 473 deletions(-) create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringBaseImpl.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringHelper.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizard.java create mode 100644 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizardPage.java diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java index 9822b32e7..1f50c07b6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java @@ -17,72 +17,34 @@ 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 sLastResFilePath = new HashMap(); - - /** 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); } /** @@ -92,17 +54,12 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage * {@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); } @@ -111,8 +68,9 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage * 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(); @@ -124,16 +82,27 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage 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. @@ -143,74 +112,31 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage 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(); @@ -231,247 +157,8 @@ class ExtractStringInputPage extends UserInputWizardPage implements IWizardPage 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> mFolderCache = - new HashMap>(); - private String mLastFolderUsedInCombo = null; - private boolean mInternalConfigChange; - private boolean mInternalFileComboChange; - - /** - * Callback invoked when the {@link ConfigurationSelector} has been changed. - *

- * The callback does the following: - *

    - *
  • Examine the current file name to retrieve the XML filename, if any. - *
  • Recompute the path based on the configuration selector (e.g. /res/values-fr/). - *
  • Examine the path to retrieve all the files in it. Keep those in a local cache. - *
  • If the XML filename from step 1 is not in the file list, it's a custom file name. - * Insert it and sort it. - *
  • Re-populate the file combo with all the choices. - *
  • 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 filePaths = mFolderCache.get(newPath); - - if (filePaths == null) { - filePaths = new TreeSet(); - - 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(); - } - } - } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java index 430ff1819..a17d81790 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java @@ -16,9 +16,9 @@ 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; @@ -64,8 +64,6 @@ import org.eclipse.text.edits.MultiTextEdit; 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; @@ -73,14 +71,9 @@ import java.io.InputStream; 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. @@ -105,8 +98,8 @@ import javax.xml.xpath.XPathExpressionException; *
  • On success, the wizard is shown, which let the user input the new ID to use. *
  • 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. *
  • 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. @@ -127,61 +120,107 @@ import javax.xml.xpath.XPathExpressionException; *
  • TODO: Have a pref in the wizard: [x] Change other Java Files *
*/ -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> 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 mChanges; + + private NewStringHelper mHelper = new NewStringHelper(); public ExtractStringRefactoring(Map 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 createArgumentMap() { HashMap args = new HashMap(); - 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 @@ -225,6 +264,11 @@ class ExtractStringRefactoring extends Refactoring { try { monitor.beginTask("Checking preconditions...", 5); + + if (mMode != Mode.EDIT_SOURCE) { + monitor.worked(5); + return status; + } if (!checkSourceFile(mFile, status, monitor)) { return status; @@ -388,7 +432,7 @@ class ExtractStringRefactoring extends Refactoring { 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) { @@ -415,9 +459,9 @@ class ExtractStringRefactoring extends Refactoring { // 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); @@ -427,12 +471,14 @@ class ExtractStringRefactoring extends Refactoring { if (status.hasError()) { return status; } - - // Prepare the change to the Java compilation unit - List 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 changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString, + status, SubMonitor.convert(monitor, 1)); + if (changes != null) { + mChanges.addAll(changes); + } } monitor.worked(1); @@ -484,6 +530,9 @@ class ExtractStringRefactoring extends Refactoring { // 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 />. @@ -866,78 +915,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>(); - } - HashSet 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 getResIdsForFile(String xmlFileWsPath) { - HashSet ids = new HashSet(); - - 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: - // - // something - // - - 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. */ @@ -950,8 +927,15 @@ class ExtractStringRefactoring extends Refactoring { /** * 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; } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java index c5b0c7d11..cfcc54621 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java @@ -25,7 +25,7 @@ import org.eclipse.ltk.ui.refactoring.RefactoringWizard; * @see ExtractStringInputPage * @see ExtractStringRefactoring */ -class ExtractStringWizard extends RefactoringWizard { +public class ExtractStringWizard extends RefactoringWizard { private final IProject mProject; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java index e141396c5..031b30382 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java @@ -17,10 +17,13 @@ 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; @@ -30,14 +33,20 @@ import org.eclipse.jface.viewers.ISelection; 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; @@ -58,21 +67,24 @@ public class ReferenceChooserDialog extends 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()); } @@ -113,13 +125,26 @@ public class ReferenceChooserDialog extends SelectionStatusDialog { // 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()); @@ -154,6 +179,7 @@ public class ReferenceChooserDialog extends SelectionStatusDialog { protected void handleSelection() { validateCurrentSelection(); + updateNewResButton(); } protected void handleDoubleClick() { @@ -205,6 +231,65 @@ public class ReferenceChooserDialog extends SelectionStatusDialog { 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. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringBaseImpl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringBaseImpl.java new file mode 100644 index 000000000..334b13387 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringBaseImpl.java @@ -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 sLastResFilePath = new HashMap(); + + /** 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> mFolderCache = + new HashMap>(); + private String mLastFolderUsedInCombo = null; + private boolean mInternalConfigChange; + private boolean mInternalFileComboChange; + + /** + * Callback invoked when the {@link ConfigurationSelector} has been changed. + *

+ * The callback does the following: + *

    + *
  • Examine the current file name to retrieve the XML filename, if any. + *
  • Recompute the path based on the configuration selector (e.g. /res/values-fr/). + *
  • Examine the path to retrieve all the files in it. Keep those in a local cache. + *
  • If the XML filename from step 1 is not in the file list, it's a custom file name. + * Insert it and sort it. + *
  • Re-populate the file combo with all the choices. + *
  • 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 filePaths = mFolderCache.get(newPath); + + if (filePaths == null) { + filePaths = new TreeSet(); + + 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(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringHelper.java new file mode 100644 index 000000000..3b8392718 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringHelper.java @@ -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> 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>(); + } + HashSet 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 getResIdsForFile(IProject project, String xmlFileWsPath) { + HashSet ids = new HashSet(); + + 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: + // + // something + // + + 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; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizard.java new file mode 100644 index 000000000..f7d8fe863 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizard.java @@ -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. + *

    + * Please do NOT override this method. + *

    + * 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; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizardPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizardPage.java new file mode 100644 index 000000000..1e2d27277 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizardPage.java @@ -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 + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java index 654e792cc..56bb8f492 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java @@ -144,7 +144,9 @@ public class UiResourceAttributeNode extends UiTextAttributeNode { return dlg.getCurrentResource(); } } else { - ReferenceChooserDialog dlg = new ReferenceChooserDialog(projectRepository, + ReferenceChooserDialog dlg = new ReferenceChooserDialog( + project, + projectRepository, shell); dlg.setCurrentResource(currentValue); -- 2.11.0