OSDN Git Service

AI 144283: ADT: Enhance Resource Chooser with ability to create new XML strings.
authorRaphael Moll <>
Thu, 2 Apr 2009 20:42:29 +0000 (13:42 -0700)
committerThe Android Open Source Project <initial-contribution@android.com>
Thu, 2 Apr 2009 20:42:29 +0000 (13:42 -0700)
  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

eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringInputPage.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/refactorings/extractstring/ExtractStringWizard.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/ui/ReferenceChooserDialog.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringBaseImpl.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringHelper.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizard.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newstring/NewStringWizardPage.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java

index 9822b32..1f50c07 100644 (file)
 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);
     }
 
     /**
@@ -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<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();
-        }
-    }
-
 }
index 430ff18..a17d817 100644 (file)
@@ -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;
  * <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.
@@ -127,61 +120,107 @@ import javax.xml.xpath.XPathExpressionException;
  * <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
@@ -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<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);
@@ -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<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.
      */
@@ -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;
     }
 
     /**
index c5b0c7d..cfcc546 100644 (file)
@@ -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;
 
index e141396..031b303 100644 (file)
 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 (file)
index 0000000..334b133
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.newstring;
+
+
+import com.android.ide.eclipse.adt.ui.ConfigurationSelector;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.HashMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NewStringBaseImpl {
+
+    public interface INewStringPageCallback {
+        /**
+         * Creates the top group with the field to replace which string and by what
+         * and by which options.
+         * 
+         * @param content A composite with a 1-column grid layout
+         * @return The {@link Text} field for the new String ID name.
+         */
+        public Text createStringGroup(Composite content);
+        
+        /** Implements {@link WizardPage#setErrorMessage(String)} */
+        public void setErrorMessage(String newMessage);
+        /** Implements {@link WizardPage#setMessage(String, int)} */
+        public void setMessage(String msg, int type);
+        /** Implements {@link WizardPage#setPageComplete(boolean)} */
+        public void setPageComplete(boolean success);
+
+        public void postValidatePage(ValidationStatus status);
+    }
+    
+    public class ValidationStatus {
+        private String mError = null;
+        private String mMessage = null;
+        public int mMessageType = WizardPage.NONE;
+
+        public boolean success() {
+            return getError() != null;
+        }
+
+        public void setError(String error) {
+            mError = error;
+            mMessageType = WizardPage.ERROR;
+        }
+
+        public String getError() {
+            return mError;
+        }
+
+        public void setMessage(String msg, int type) {
+            mMessage = msg;
+            mMessageType = type;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+        
+        public int getMessageType() {
+            return mMessageType;
+        }
+    }
+    
+    /** Last res file path used, shared across the session instances but specific to the
+     *  current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */
+    private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>();
+
+    /** The project where the user selection happened. */
+    private final IProject mProject;
+    /** Text field where the user enters the new ID. */ 
+    private Text mStringIdField;
+    /** The configuration selector, to select the resource path of the XML file. */
+    private ConfigurationSelector mConfigSelector;
+    /** The combo to display the existing XML files or enter a new one. */
+    private Combo mResFileCombo;
+    
+    private NewStringHelper mHelper = new NewStringHelper();
+
+    /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and
+     *  a leaf file name ending with .xml */
+    private static final Pattern RES_XML_FILE_REGEX = Pattern.compile(
+                                     "/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml");  //$NON-NLS-1$
+    /** Absolute destination folder root, e.g. "/res/" */
+    private static final String RES_FOLDER_ABS =
+        AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
+    /** Relative destination folder root, e.g. "res/" */
+    private static final String RES_FOLDER_REL =
+        SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
+
+    private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml";  //$NON-NLS-1$
+
+    private final INewStringPageCallback mWizardPage;
+    
+    public NewStringBaseImpl(IProject project, INewStringPageCallback wizardPage) {
+        mProject = project;
+        mWizardPage = wizardPage;
+    }
+
+    /**
+     * Create the UI for the new string wizard.
+     */
+    public void createControl(Composite parent) {
+        mStringIdField = mWizardPage.createStringGroup(parent);
+        createResFileGroup(parent);
+    }
+
+    /**
+     * Creates the lower group with the fields to choose the resource confirmation and
+     * the target XML file.
+     * 
+     * @param content A composite with a 1-column grid layout
+     */
+    private void createResFileGroup(Composite content) {
+
+        Group group = new Group(content, SWT.NONE);
+        group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        group.setText("XML resource to edit");
+
+        GridLayout layout = new GridLayout();
+        layout.numColumns = 2;
+        group.setLayout(layout);
+        
+        // line: selection of the res config
+
+        Label label;
+        label = new Label(group, SWT.NONE);
+        label.setText("Configuration:");
+
+        mConfigSelector = new ConfigurationSelector(group);
+        GridData gd = new GridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+        gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+        gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+        mConfigSelector.setLayoutData(gd);
+        OnConfigSelectorUpdated onConfigSelectorUpdated = new OnConfigSelectorUpdated();
+        mConfigSelector.setOnChangeListener(onConfigSelectorUpdated);
+        
+        // line: selection of the output file
+
+        label = new Label(group, SWT.NONE);
+        label.setText("Resource file:");
+
+        mResFileCombo = new Combo(group, SWT.DROP_DOWN);
+        mResFileCombo.select(0);
+        mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+        mResFileCombo.addModifyListener(onConfigSelectorUpdated);
+
+        // set output file name to the last one used
+        
+        String projPath = mProject.getFullPath().toPortableString();
+        String filePath = sLastResFilePath.get(projPath);
+        
+        mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH);
+        onConfigSelectorUpdated.run();
+    }
+
+    /**
+     * Validates fields of the wizard input page. Displays errors as appropriate and
+     * enable the "Next" button (or not) by calling
+     * {@link INewStringPageCallback#setPageComplete(boolean)}.
+     * 
+     * @return True if the page has been positively validated. It may still have warnings.
+     */
+    public boolean validatePage() {
+        ValidationStatus status = new ValidationStatus();
+
+        validateStringFields(status);
+        if (status.success()) {
+            validatePathFields(status);
+        }
+        
+        mWizardPage.postValidatePage(status);
+
+        mWizardPage.setErrorMessage(status.getError());
+        mWizardPage.setMessage(status.getMessage(), status.getMessageType());
+        mWizardPage.setPageComplete(status.success());
+        return status.success();
+    }
+     
+    public void validateStringFields(ValidationStatus status) {
+
+        String text = mStringIdField.getText().trim();
+        if (text == null || text.length() < 1) {
+            status.setError("Please provide a resource ID to replace with.");
+        } else {
+            for (int i = 0; i < text.length(); i++) {
+                char c = text.charAt(i);
+                boolean ok = i == 0 ?
+                        Character.isJavaIdentifierStart(c) :
+                        Character.isJavaIdentifierPart(c);
+                if (!ok) {
+                    status.setError(String.format(
+                            "The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.",
+                            c, i+1));
+                    break;
+                }
+            }
+        }
+    }
+
+    public ValidationStatus validatePathFields(ValidationStatus status) {
+        String resFile = getResFileProjPath();
+        if (resFile == null || resFile.length() == 0) {
+            status.setError("A resource file name is required.");
+        } else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) {
+            status.setError("The XML file name is not valid.");
+        }
+        
+        if (status.success()) {
+            sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
+
+            String text = mStringIdField.getText().trim();
+
+            if (mHelper.isResIdDuplicate(mProject, resFile, text)) {
+                status.setMessage(
+                        String.format("There's already a string item called '%1$s' in %2$s.",
+                                text, resFile), WizardPage.WARNING);
+            } else if (mProject.findMember(resFile) == null) {
+                status.setMessage(
+                        String.format("File %2$s does not exist and will be created.",
+                                text, resFile), WizardPage.INFORMATION);
+            }
+        }
+
+        return status;
+    }
+    
+    public String getResFileProjPath() {
+        return mResFileCombo.getText().trim();
+    }
+
+    public class OnConfigSelectorUpdated implements Runnable, ModifyListener {
+        
+        /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */
+        private final Pattern mPathRegex = Pattern.compile(
+            "(/res/[a-z][a-zA-Z0-9_-]+/)(.+)");  //$NON-NLS-1$
+
+        /** Temporary config object used to retrieve the Config Selector value. */
+        private FolderConfiguration mTempConfig = new FolderConfiguration();
+
+        private HashMap<String, TreeSet<String>> mFolderCache =
+            new HashMap<String, TreeSet<String>>();
+        private String mLastFolderUsedInCombo = null;
+        private boolean mInternalConfigChange;
+        private boolean mInternalFileComboChange;
+
+        /**
+         * Callback invoked when the {@link ConfigurationSelector} has been changed.
+         * <p/>
+         * The callback does the following:
+         * <ul>
+         * <li> Examine the current file name to retrieve the XML filename, if any.
+         * <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/).
+         * <li> Examine the path to retrieve all the files in it. Keep those in a local cache.
+         * <li> If the XML filename from step 1 is not in the file list, it's a custom file name.
+         *      Insert it and sort it.
+         * <li> Re-populate the file combo with all the choices.
+         * <li> Select the original XML file. 
+         */
+        public void run() {
+            if (mInternalConfigChange) {
+                return;
+            }
+            
+            // get current leafname, if any
+            String leafName = "";  //$NON-NLS-1$
+            String currPath = mResFileCombo.getText();
+            Matcher m = mPathRegex.matcher(currPath);
+            if (m.matches()) {
+                // Note: groups 1 and 2 cannot be null.
+                leafName = m.group(2);
+                currPath = m.group(1);
+            } else {
+                // There was a path but it was invalid. Ignore it.
+                currPath = "";  //$NON-NLS-1$
+            }
+
+            // recreate the res path from the current configuration
+            mConfigSelector.getConfiguration(mTempConfig);
+            StringBuffer sb = new StringBuffer(RES_FOLDER_ABS);
+            sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES));
+            sb.append('/');
+
+            String newPath = sb.toString();
+            if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) {
+                // Path has not changed. No need to reload.
+                return;
+            }
+            
+            // Get all the files at the new path
+
+            TreeSet<String> filePaths = mFolderCache.get(newPath);
+            
+            if (filePaths == null) {
+                filePaths = new TreeSet<String>();
+
+                IFolder folder = mProject.getFolder(newPath);
+                if (folder != null && folder.exists()) {
+                    try {
+                        for (IResource res : folder.members()) {
+                            String name = res.getName();
+                            if (res.getType() == IResource.FILE && name.endsWith(".xml")) {  //$NON-NLS-1$
+                                filePaths.add(newPath + name);
+                            }
+                        }
+                    } catch (CoreException e) {
+                        // Ignore.
+                    }
+                }
+                
+                mFolderCache.put(newPath, filePaths);
+            }
+
+            currPath = newPath + leafName;
+            if (leafName.length() > 0 && !filePaths.contains(currPath)) {
+                filePaths.add(currPath);
+            }
+            
+            // Fill the combo
+            try {
+                mInternalFileComboChange = true;
+                
+                mResFileCombo.removeAll();
+                
+                for (String filePath : filePaths) {
+                    mResFileCombo.add(filePath);
+                }
+
+                int index = -1;
+                if (leafName.length() > 0) {
+                    index = mResFileCombo.indexOf(currPath);
+                    if (index >= 0) {
+                        mResFileCombo.select(index);
+                    }
+                }
+                
+                if (index == -1) {
+                    mResFileCombo.setText(currPath);
+                }
+                
+                mLastFolderUsedInCombo = newPath;
+                
+            } finally {
+                mInternalFileComboChange = false;
+            }
+
+            // finally validate the whole page
+            validatePage();
+        }
+
+        /**
+         * Callback invoked when {@link NewStringBaseImpl#mResFileCombo} has been
+         * modified.
+         */
+        public void modifyText(ModifyEvent e) {
+            if (mInternalFileComboChange) {
+                return;
+            }
+
+            String wsFolderPath = mResFileCombo.getText();
+
+            // This is a custom path, we need to sanitize it.
+            // First it should start with "/res/". Then we need to make sure there are no
+            // relative paths, things like "../" or "./" or even "//".
+            wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/");  //$NON-NLS-1$ //$NON-NLS-2$
+            wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", "");                   //$NON-NLS-1$ //$NON-NLS-2$
+            wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", "");               //$NON-NLS-1$ //$NON-NLS-2$
+
+            // We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
+            if (wsFolderPath.startsWith(RES_FOLDER_REL)) {
+                wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length());
+                
+                mInternalFileComboChange = true;
+                mResFileCombo.setText(wsFolderPath);
+                mInternalFileComboChange = false;
+            }
+
+            if (wsFolderPath.startsWith(RES_FOLDER_ABS)) {
+                wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length());
+                
+                int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
+                if (pos >= 0) {
+                    wsFolderPath = wsFolderPath.substring(0, pos);
+                }
+
+                String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
+
+                if (folderSegments.length > 0) {
+                    String folderName = folderSegments[0];
+
+                    if (folderName != null && !folderName.equals(wsFolderPath)) {
+                        // update config selector
+                        mInternalConfigChange = true;
+                        mConfigSelector.setConfiguration(folderSegments);
+                        mInternalConfigChange = false;
+                    }
+                }
+            }
+
+            validatePage();
+        }
+    }
+}
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 (file)
index 0000000..3b83927
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.newstring;
+
+import com.android.ide.eclipse.common.project.AndroidXPathFactory;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * 
+ */
+public class NewStringHelper {
+
+    /** A temporary cache of R.string IDs defined by a given xml file. The key is the
+     * project path of the file, the data is a set of known string Ids for that file. */
+    private HashMap<String,HashSet<String>> mResIdCache;
+    /** An instance of XPath, created lazily on demand. */
+    private XPath mXPath;
+
+    public NewStringHelper() {
+    }
+    
+    /**
+     * Utility method used by the wizard to check whether the given string ID is already
+     * defined in the XML file which path is given.
+     * 
+     * @param project The project contain the XML file. 
+     * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml".
+     *          The given file may or may not exist.
+     * @param stringId The string ID to find.
+     * @return True if such a string ID is already defined.
+     */
+    public boolean isResIdDuplicate(IProject project, String xmlFileWsPath, String stringId) {
+        // This is going to be called many times on the same file.
+        // Build a cache of the existing IDs for a given file.
+        if (mResIdCache == null) {
+            mResIdCache = new HashMap<String, HashSet<String>>();
+        }
+        HashSet<String> cache = mResIdCache.get(xmlFileWsPath);
+        if (cache == null) {
+            cache = getResIdsForFile(project, xmlFileWsPath);
+            mResIdCache.put(xmlFileWsPath, cache);
+        }
+        
+        return cache.contains(stringId);
+    }
+
+    /**
+     * Extract all the defined string IDs from a given file using XPath.
+     * @param project The project contain the XML file. 
+     * @param xmlFileWsPath The project path of the file to parse. It may not exist.
+     * @return The set of all string IDs defined in the file. The returned set is always non
+     *   null. It is empty if the file does not exist.
+     */
+    private HashSet<String> getResIdsForFile(IProject project, String xmlFileWsPath) {
+        HashSet<String> ids = new HashSet<String>();
+        
+        if (mXPath == null) {
+            mXPath = AndroidXPathFactory.newXPath();
+        }
+
+        // Access the project that contains the resource that contains the compilation unit
+        IResource resource = project.getFile(xmlFileWsPath);
+        
+        if (resource != null && resource.exists() && resource.getType() == IResource.FILE) {
+            InputSource source;
+            try {
+                source = new InputSource(((IFile) resource).getContents());
+
+                // We want all the IDs in an XML structure like this:
+                // <resources>
+                //    <string name="ID">something</string>
+                // </resources>
+                
+                String xpathExpr = "/resources/string/@name";   //$NON-NLS-1$
+                
+                Object result = mXPath.evaluate(xpathExpr, source, XPathConstants.NODESET);
+                if (result instanceof NodeList) {
+                    NodeList list = (NodeList) result;
+                    for (int n = list.getLength() - 1; n >= 0; n--) {
+                        String id = list.item(n).getNodeValue();
+                        ids.add(id);
+                    }
+                }
+                
+            } catch (CoreException e1) {
+                // IFile.getContents failed. Ignore.
+            } catch (XPathExpressionException e) {
+                // mXPath.evaluate failed. Ignore.
+            }
+        }
+        
+        return ids;
+    }
+
+}
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 (file)
index 0000000..f7d8fe8
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.newstring;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.Wizard;
+
+/**
+ * 
+ */
+public class NewStringWizard extends Wizard {
+
+    protected static final String MAIN_PAGE_NAME = "newXmlStringPage"; //$NON-NLS-1$
+
+    private NewStringWizardPage mMainPage;
+
+    public NewStringWizard(IProject project) {
+        super();
+        
+        mMainPage = createMainPage(project);
+    }
+    
+    /**
+     * Creates the wizard page.
+     * <p/>
+     * Please do NOT override this method.
+     * <p/>
+     * This is protected so that it can be overridden by unit tests.
+     * However the contract of this class is private and NO ATTEMPT will be made
+     * to maintain compatibility between different versions of the plugin.
+     * @param project 
+     */
+    protected NewStringWizardPage createMainPage(IProject project) {
+        return new NewStringWizardPage(project, MAIN_PAGE_NAME);
+    }
+    
+    @Override
+    public void addPages() {
+        addPage(mMainPage);
+        super.addPages();
+    }
+
+    /**
+     * @see org.eclipse.jface.wizard.Wizard#performFinish()
+     */
+    @Override
+    public boolean performFinish() {
+        // pass
+        return false;
+    }
+
+}
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 (file)
index 0000000..1e2d272
--- /dev/null
@@ -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
+    }
+}
index 654e792..56bb8f4 100644 (file)
@@ -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);