1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.ide.eclipse.adt.internal.wizards.newxmlfile; 17 18 import com.android.SdkConstants; 19 import com.android.ide.common.resources.configuration.ResourceQualifier; 20 import com.android.ide.eclipse.adt.AdtConstants; 21 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector; 22 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.ConfigurationState; 23 import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode; 24 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo; 25 import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard.Values; 26 27 import org.eclipse.core.resources.IFile; 28 import org.eclipse.jface.dialogs.IMessageProvider; 29 import org.eclipse.jface.wizard.IWizardPage; 30 import org.eclipse.jface.wizard.WizardPage; 31 import org.eclipse.swt.SWT; 32 import org.eclipse.swt.events.ModifyEvent; 33 import org.eclipse.swt.events.ModifyListener; 34 import org.eclipse.swt.layout.GridData; 35 import org.eclipse.swt.layout.GridLayout; 36 import org.eclipse.swt.widgets.Composite; 37 import org.eclipse.swt.widgets.Label; 38 import org.eclipse.swt.widgets.Text; 39 40 /** 41 * Second page of the {@link NewXmlFileWizard}. 42 * <p> 43 * This page is used for choosing the current configuration or specific resource 44 * folder. 45 */ 46 public class ChooseConfigurationPage extends WizardPage { 47 private Values mValues; 48 private Text mWsFolderPathTextField; 49 private ConfigurationSelector mConfigSelector; 50 private boolean mInternalWsFolderPathUpdate; 51 private boolean mInternalConfigSelectorUpdate; 52 53 /** Absolute destination folder root, e.g. "/res/" */ 54 static final String RES_FOLDER_ABS = AdtConstants.WS_RESOURCES + AdtConstants.WS_SEP; 55 /** Relative destination folder root, e.g. "res/" */ 56 static final String RES_FOLDER_REL = SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP; 57 58 /** 59 * Create the wizard. 60 * 61 * @param values value object holding current wizard state 62 */ 63 public ChooseConfigurationPage(NewXmlFileWizard.Values values) { 64 super("chooseConfig"); 65 mValues = values; 66 setTitle("Choose Configuration Folder"); 67 } 68 69 @Override 70 public void setVisible(boolean visible) { 71 super.setVisible(visible); 72 if (visible) { 73 if (mValues.folderPath != null) { 74 mWsFolderPathTextField.setText(mValues.folderPath); 75 } 76 } 77 } 78 79 @Override 80 public void createControl(Composite parent) { 81 // This UI is maintained with WindowBuilder. 82 83 Composite composite = new Composite(parent, SWT.NULL); 84 composite.setLayout(new GridLayout(2, false /* makeColumnsEqualWidth */)); 85 composite.setLayoutData(new GridData(GridData.FILL_BOTH)); 86 87 // label before configuration selector 88 Label label = new Label(composite, SWT.NONE); 89 label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); 90 label.setText("Optional: Choose a specific configuration to limit the XML to:"); 91 92 // configuration selector 93 mConfigSelector = new ConfigurationSelector(composite, SelectorMode.DEFAULT); 94 GridData gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); 95 gd.verticalAlignment = SWT.FILL; 96 gd.horizontalAlignment = SWT.FILL; 97 gd.horizontalSpan = 2; 98 gd.heightHint = ConfigurationSelector.HEIGHT_HINT; 99 mConfigSelector.setLayoutData(gd); 100 mConfigSelector.setOnChangeListener(new ConfigurationChangeListener()); 101 102 // Folder name: [text] 103 String tooltip = "The folder where the file will be generated, relative to the project."; 104 105 Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL); 106 GridData gdSeparator = new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1); 107 gdSeparator.heightHint = 10; 108 separator.setLayoutData(gdSeparator); 109 Label folderLabel = new Label(composite, SWT.NONE); 110 folderLabel.setText("Folder:"); 111 folderLabel.setToolTipText(tooltip); 112 113 mWsFolderPathTextField = new Text(composite, SWT.BORDER); 114 mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 115 mWsFolderPathTextField.setToolTipText(tooltip); 116 mWsFolderPathTextField.addModifyListener(new ModifyListener() { 117 @Override 118 public void modifyText(ModifyEvent e) { 119 onWsFolderPathUpdated(); 120 } 121 }); 122 123 setControl(composite); 124 125 mConfigSelector.setConfiguration(mValues.configuration); 126 } 127 128 /** 129 * Callback called when the Folder text field is changed, either programmatically 130 * or by the user. 131 */ 132 private void onWsFolderPathUpdated() { 133 if (mInternalWsFolderPathUpdate) { 134 return; 135 } 136 137 String wsFolderPath = mWsFolderPathTextField.getText(); 138 139 // This is a custom path, we need to sanitize it. 140 // First it should start with "/res/". Then we need to make sure there are no 141 // relative paths, things like "../" or "./" or even "//". 142 wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$ 143 wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$ 144 wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ 145 146 // We get "res/foo" from selections relative to the project when we want a "/res/foo" path. 147 if (wsFolderPath.startsWith(RES_FOLDER_REL)) { 148 wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length()); 149 150 mInternalWsFolderPathUpdate = true; 151 mWsFolderPathTextField.setText(wsFolderPath); 152 mInternalWsFolderPathUpdate = false; 153 } 154 155 mValues.folderPath = wsFolderPath; 156 157 if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { 158 wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); 159 160 int pos = wsFolderPath.indexOf(AdtConstants.WS_SEP_CHAR); 161 if (pos >= 0) { 162 wsFolderPath = wsFolderPath.substring(0, pos); 163 } 164 165 String[] folderSegments = wsFolderPath.split(SdkConstants.RES_QUALIFIER_SEP); 166 167 if (folderSegments.length > 0) { 168 String folderName = folderSegments[0]; 169 170 // update config selector 171 mInternalConfigSelectorUpdate = true; 172 mConfigSelector.setConfiguration(folderSegments); 173 mInternalConfigSelectorUpdate = false; 174 175 IWizardPage previous = ((NewXmlFileWizard) getWizard()).getPreviousPage(this); 176 if (previous instanceof NewXmlFileCreationPage) { 177 NewXmlFileCreationPage p = (NewXmlFileCreationPage) previous; 178 p.selectTypeFromFolder(folderName); 179 } 180 } 181 } 182 183 validatePage(); 184 } 185 186 /** 187 * Callback called when the configuration has changed in the {@link ConfigurationSelector}. 188 */ 189 private class ConfigurationChangeListener implements Runnable { 190 @Override 191 public void run() { 192 if (mInternalConfigSelectorUpdate) { 193 return; 194 } 195 196 resetFolderPath(true /*validate*/); 197 } 198 } 199 200 /** 201 * Reset the current Folder path based on the UI selection 202 * @param validate if true, force a call to {@link #validatePage()}. 203 */ 204 private void resetFolderPath(boolean validate) { 205 TypeInfo type = mValues.type; 206 if (type != null) { 207 mConfigSelector.getConfiguration(mValues.configuration); 208 StringBuilder sb = new StringBuilder(RES_FOLDER_ABS); 209 sb.append(mValues.configuration.getFolderName(type.getResFolderType())); 210 211 mInternalWsFolderPathUpdate = true; 212 String newPath = sb.toString(); 213 mValues.folderPath = newPath; 214 mWsFolderPathTextField.setText(newPath); 215 mInternalWsFolderPathUpdate = false; 216 217 if (validate) { 218 validatePage(); 219 } 220 } 221 } 222 223 /** 224 * Returns the destination folder path relative to the project or an empty string. 225 * 226 * @return the currently edited folder 227 */ 228 public String getWsFolderPath() { 229 return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$ 230 } 231 232 /** 233 * Validates the fields, displays errors and warnings. 234 * Enables the finish button if there are no errors. 235 */ 236 private void validatePage() { 237 String error = null; 238 String warning = null; 239 240 // -- validate folder configuration 241 if (error == null) { 242 ConfigurationState state = mConfigSelector.getState(); 243 if (state == ConfigurationState.INVALID_CONFIG) { 244 ResourceQualifier qual = mConfigSelector.getInvalidQualifier(); 245 if (qual != null) { 246 error = 247 String.format("The qualifier '%1$s' is invalid in the folder configuration.", 248 qual.getName()); 249 } 250 } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) { 251 error = "The Region qualifier requires the Language qualifier."; 252 } 253 } 254 255 // -- validate generated path 256 if (error == null) { 257 String wsFolderPath = getWsFolderPath(); 258 if (!wsFolderPath.startsWith(RES_FOLDER_ABS)) { 259 error = String.format("Target folder must start with %1$s.", RES_FOLDER_ABS); 260 } 261 } 262 263 // -- validate destination file doesn't exist 264 if (error == null) { 265 IFile file = mValues.getDestinationFile(); 266 if (file != null && file.exists()) { 267 warning = "The destination file already exists"; 268 } 269 } 270 271 // -- update UI & enable finish if there's no error 272 setPageComplete(error == null); 273 if (error != null) { 274 setMessage(error, IMessageProvider.ERROR); 275 } else if (warning != null) { 276 setMessage(warning, IMessageProvider.WARNING); 277 } else { 278 setErrorMessage(null); 279 setMessage(null); 280 } 281 } 282 } 283