aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java606
1 files changed, 606 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java
new file mode 100644
index 000000000..5ac5f5c4e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java
@@ -0,0 +1,606 @@
+/*
+ * 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.internal.refactorings.extractstring;
+
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector;
+import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode;
+import com.android.resources.ResourceFolderType;
+
+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.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.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @see ExtractStringRefactoring
+ */
+class ExtractStringInputPage extends UserInputWizardPage {
+
+ /** 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 to be generated or replaced with. */
+ private Combo mStringIdCombo;
+ /** Text field where the user enters the new string value. */
+ private Text mStringValueField;
+ /** 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;
+ /** Checkbox asking whether to replace in all Java files. */
+ private Button mReplaceAllJava;
+ /** Checkbox asking whether to replace in all XML files with same name but other res config */
+ private Button mReplaceAllXml;
+
+ /** 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 =
+ AdtConstants.WS_RESOURCES + AdtConstants.WS_SEP;
+ /** Relative destination folder root, e.g. "res/" */
+ private static final String RES_FOLDER_REL =
+ SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP;
+
+ private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml"; //$NON-NLS-1$
+
+ private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper();
+
+ private final OnConfigSelectorUpdated mOnConfigSelectorUpdated = new OnConfigSelectorUpdated();
+
+ private ModifyListener mValidateOnModify = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ };
+
+ private SelectionListener mValidateOnSelection = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ }
+ };
+
+ public ExtractStringInputPage(IProject project) {
+ super("ExtractStringInputPage"); //$NON-NLS-1$
+ mProject = project;
+ }
+
+ /**
+ * Create the UI for the refactoring wizard.
+ * <p/>
+ * Note that at that point the initial conditions have been checked in
+ * {@link ExtractStringRefactoring}.
+ * <p/>
+ *
+ * Note: the special tag below defines this as the entry point for the WindowsDesigner Editor.
+ * @wbp.parser.entryPoint
+ */
+ @Override
+ public void createControl(Composite parent) {
+ Composite content = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ content.setLayout(layout);
+
+ createStringGroup(content);
+ createResFileGroup(content);
+ createOptionGroup(content);
+
+ initUi();
+ 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
+ */
+ public void createStringGroup(Composite content) {
+
+ final ExtractStringRefactoring ref = getOurRefactoring();
+
+ Group group = new Group(content, SWT.NONE);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ group.setText("New String");
+ if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) {
+ group.setText("String Replacement");
+ }
+
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+
+ // line: Textfield for string value (based on selection, if any)
+
+ Label label = new Label(group, SWT.NONE);
+ label.setText("&String");
+
+ String selectedString = ref.getTokenString();
+
+ mStringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ mStringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mStringValueField.setText(selectedString != null ? selectedString : ""); //$NON-NLS-1$
+
+ ref.setNewStringValue(mStringValueField.getText());
+
+ mStringValueField.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ });
+
+ // line : Textfield for new ID
+
+ label = new Label(group, SWT.NONE);
+ label.setText("ID &R.string.");
+ if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) {
+ label.setText("&Replace by R.string.");
+ } else if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) {
+ label.setText("New &R.string.");
+ }
+
+ mStringIdCombo = new Combo(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.DROP_DOWN);
+ mStringIdCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mStringIdCombo.setText(guessId(selectedString));
+ mStringIdCombo.forceFocus();
+
+ ref.setNewStringId(mStringIdCombo.getText().trim());
+
+ mStringIdCombo.addModifyListener(mValidateOnModify);
+ mStringIdCombo.addSelectionListener(mValidateOnSelection);
+ }
+
+ /**
+ * 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);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.grabExcessVerticalSpace = true;
+ group.setLayoutData(gd);
+ 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, SelectorMode.DEFAULT);
+ gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+ gd.horizontalSpan = 2;
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mConfigSelector.setLayoutData(gd);
+ mConfigSelector.setOnChangeListener(mOnConfigSelectorUpdated);
+
+ // 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(mOnConfigSelectorUpdated);
+ }
+
+ /**
+ * Creates the bottom option groups with a few checkboxes.
+ *
+ * @param content A composite with a 1-column grid layout
+ */
+ private void createOptionGroup(Composite content) {
+ Group options = new Group(content, SWT.NONE);
+ options.setText("Options");
+ GridData gd_Options = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
+ gd_Options.widthHint = 77;
+ options.setLayoutData(gd_Options);
+ options.setLayout(new GridLayout(1, false));
+
+ mReplaceAllJava = new Button(options, SWT.CHECK);
+ mReplaceAllJava.setToolTipText("When checked, the exact same string literal will be replaced in all Java files.");
+ mReplaceAllJava.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mReplaceAllJava.setText("Replace in all &Java files");
+ mReplaceAllJava.addSelectionListener(mValidateOnSelection);
+
+ mReplaceAllXml = new Button(options, SWT.CHECK);
+ mReplaceAllXml.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mReplaceAllXml.setToolTipText("When checked, string literals will be replaced in other XML resource files having the same name but located in different resource configuration folders.");
+ mReplaceAllXml.setText("Replace in all &XML files for different configuration");
+ mReplaceAllXml.addSelectionListener(mValidateOnSelection);
+ }
+
+ // -- Start of internal part ----------
+ // Hide everything down-below from WindowsDesigner Editor
+ //$hide>>$
+
+ /**
+ * Init UI just after it has been created the first time.
+ */
+ private void initUi() {
+ // 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);
+ mOnConfigSelectorUpdated.run();
+ validatePage();
+ }
+
+ /**
+ * Utility method to guess a suitable new XML ID based on the selected string.
+ */
+ public static String guessId(String text) {
+ if (text == null) {
+ return ""; //$NON-NLS-1$
+ }
+
+ // make lower case
+ text = text.toLowerCase(Locale.US);
+
+ // everything not alphanumeric becomes an underscore
+ text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // the id must be a proper Java identifier, so it can't start with a number
+ if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) {
+ text = "_" + text; //$NON-NLS-1$
+ }
+ return text;
+ }
+
+ /**
+ * Returns the {@link ExtractStringRefactoring} instance used by this wizard page.
+ */
+ private ExtractStringRefactoring getOurRefactoring() {
+ 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)}.
+ *
+ * If validation succeeds, this updates the text id & value in the refactoring object.
+ *
+ * @return True if the page has been positively validated. It may still have warnings.
+ */
+ private boolean validatePage() {
+ boolean success = true;
+
+ ExtractStringRefactoring ref = getOurRefactoring();
+
+ ref.setReplaceAllJava(mReplaceAllJava.getSelection());
+ ref.setReplaceAllXml(mReplaceAllXml.isEnabled() && mReplaceAllXml.getSelection());
+
+ // Analyze fatal errors.
+
+ String text = mStringIdCombo.getText().trim();
+ if (text == null || text.length() < 1) {
+ setErrorMessage("Please provide a resource ID.");
+ 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;
+ }
+ }
+
+ // update the field in the refactoring object in case of success
+ if (success) {
+ ref.setNewStringId(text);
+ }
+ }
+
+ 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);
+
+ ref.setTargetFile(resFile);
+ sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile);
+
+ String idValue = mXmlHelper.valueOfStringId(mProject, resFile, text);
+ if (idValue != null) {
+ String msg = String.format("%1$s already contains a string ID '%2$s' with value '%3$s'.",
+ resFile,
+ text,
+ idValue);
+ if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) {
+ setErrorMessage(msg);
+ success = false;
+ } else {
+ setMessage(msg, 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);
+ }
+ }
+
+ if (success) {
+ // Also update the text value in case of success.
+ ref.setNewStringValue(mStringValueField.getText());
+ }
+
+ setPageComplete(success);
+ return success;
+ }
+
+ private void updateStringValueCombo() {
+ String resFile = mResFileCombo.getText();
+ Map<String, String> ids = mXmlHelper.getResIdsForFile(mProject, resFile);
+
+ // get the current text from the combo, to make sure we don't change it
+ String currText = mStringIdCombo.getText();
+
+ // erase the choices and fill with the given ids
+ mStringIdCombo.removeAll();
+ mStringIdCombo.setItems(ids.keySet().toArray(new String[ids.size()]));
+
+ // set the current text to preserve it in case it changed
+ if (!currText.equals(mStringIdCombo.getText())) {
+ mStringIdCombo.setText(currText);
+ }
+ }
+
+ private 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.
+ */
+ @Override
+ 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(AdtConstants.WS_SEP);
+
+ 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
+ updateStringValueCombo();
+ validatePage();
+ }
+
+ /**
+ * Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been
+ * modified.
+ */
+ @Override
+ 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(AdtConstants.WS_SEP_CHAR);
+ if (pos >= 0) {
+ wsFolderPath = wsFolderPath.substring(0, pos);
+ }
+
+ String[] folderSegments = wsFolderPath.split(SdkConstants.RES_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;
+ }
+ }
+ }
+
+ updateStringValueCombo();
+ validatePage();
+ }
+ }
+
+ // End of hiding from SWT Designer
+ //$hide<<$
+
+}