1 /* 2 * Copyright (C) 2010 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 17 package com.android.ide.eclipse.adt.internal.editors.export; 18 19 import com.android.SdkConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; 22 23 import org.eclipse.jface.text.BadLocationException; 24 import org.eclipse.jface.text.DocumentEvent; 25 import org.eclipse.jface.text.IDocument; 26 import org.eclipse.jface.text.IRegion; 27 import org.eclipse.swt.events.ModifyEvent; 28 import org.eclipse.swt.events.ModifyListener; 29 import org.eclipse.swt.widgets.Composite; 30 import org.eclipse.swt.widgets.Control; 31 import org.eclipse.swt.widgets.Text; 32 import org.eclipse.ui.forms.widgets.FormToolkit; 33 import org.eclipse.ui.forms.widgets.Section; 34 35 import java.util.HashMap; 36 import java.util.HashSet; 37 import java.util.Iterator; 38 39 /** 40 * Section part for editing fields of a properties file in an Export editor. 41 * <p/> 42 * This base class is intended to be derived and customized. 43 */ 44 abstract class AbstractPropertiesFieldsPart extends ManifestSectionPart { 45 46 private final HashMap<String, Control> mNameToField = new HashMap<String, Control>(); 47 48 private ExportEditor mEditor; 49 50 private boolean mInternalTextUpdate = false; 51 52 public AbstractPropertiesFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) { 53 super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); 54 mEditor = editor; 55 } 56 57 protected HashMap<String, Control> getNameToField() { 58 return mNameToField; 59 } 60 61 protected ExportEditor getEditor() { 62 return mEditor; 63 } 64 65 protected void setInternalTextUpdate(boolean internalTextUpdate) { 66 mInternalTextUpdate = internalTextUpdate; 67 } 68 69 protected boolean isInternalTextUpdate() { 70 return mInternalTextUpdate; 71 } 72 73 /** 74 * Adds a modify listener to every text field that will mark the part as dirty. 75 * 76 * CONTRACT: Derived classes MUST call this at the end of their constructor. 77 * 78 * @see #setFieldModifyListener(Control, ModifyListener) 79 */ 80 protected void addModifyListenerToFields() { 81 ModifyListener markDirtyListener = new ModifyListener() { 82 @Override 83 public void modifyText(ModifyEvent e) { 84 // Mark the part as dirty if a field has been changed. 85 // This will force a commit() operation to store the data in the model. 86 if (!mInternalTextUpdate) { 87 markDirty(); 88 } 89 } 90 }; 91 92 for (Control field : mNameToField.values()) { 93 setFieldModifyListener(field, markDirtyListener); 94 } 95 } 96 97 /** 98 * Sets a listener that will mark the part as dirty when the control is modified. 99 * The base method only handles {@link Text} fields. 100 * 101 * CONTRACT: Derived classes CAN use this to add a listener to their own controls. 102 * The listener must call {@link #markDirty()} when the control is modified by the user. 103 * 104 * @param field A control previously registered with {@link #getNameToField()}. 105 * @param markDirtyListener A {@link ModifyListener} that invokes {@link #markDirty()}. 106 * 107 * @see #isInternalTextUpdate() 108 */ 109 protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) { 110 if (field instanceof Text) { 111 ((Text) field).addModifyListener(markDirtyListener); 112 } 113 } 114 115 /** 116 * Updates the model based on the content of fields. This is invoked when a field 117 * has marked the document as dirty. 118 * 119 * CONTRACT: Derived classes do not need to override this. 120 */ 121 @Override 122 public void commit(boolean onSave) { 123 124 // We didn't store any information indicating which field was dirty (we could). 125 // Since there are not many fields, just update all the document lines that 126 // match our field keywords. 127 128 if (isDirty()) { 129 mEditor.wrapRewriteSession(new Runnable() { 130 @Override 131 public void run() { 132 saveFieldsToModel(); 133 } 134 }); 135 } 136 137 super.commit(onSave); 138 } 139 140 private void saveFieldsToModel() { 141 // Get a list of all keywords to process. Go thru the document, replacing in-place 142 // the ones we can find and remove them from this set. This will leave the list 143 // of new keywords to add at the end of the document. 144 HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet()); 145 146 IDocument doc = mEditor.getDocument(); 147 int numLines = doc.getNumberOfLines(); 148 149 String delim = null; 150 try { 151 delim = numLines > 0 ? doc.getLineDelimiter(0) : null; 152 } catch (BadLocationException e1) { 153 // ignore 154 } 155 if (delim == null || delim.length() == 0) { 156 delim = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS ? 157 "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2# 158 } 159 160 for (int i = 0; i < numLines; i++) { 161 try { 162 IRegion info = doc.getLineInformation(i); 163 String line = doc.get(info.getOffset(), info.getLength()); 164 line = line.trim(); 165 if (line.startsWith("#")) { //$NON-NLS-1$ 166 continue; 167 } 168 169 int pos = line.indexOf('='); 170 if (pos > 0 && pos < line.length() - 1) { 171 String key = line.substring(0, pos).trim(); 172 173 Control field = mNameToField.get(key); 174 if (field != null) { 175 176 // This is the new line to inject 177 line = key + "=" + getFieldText(field); 178 179 try { 180 // replace old line by new one. This doesn't change the 181 // line delimiter. 182 mInternalTextUpdate = true; 183 doc.replace(info.getOffset(), info.getLength(), line); 184 allKeywords.remove(key); 185 } finally { 186 mInternalTextUpdate = false; 187 } 188 } 189 } 190 191 } catch (BadLocationException e) { 192 // TODO log it 193 AdtPlugin.log(e, "Failed to replace in export.properties"); 194 } 195 } 196 197 for (String key : allKeywords) { 198 Control field = mNameToField.get(key); 199 if (field != null) { 200 // This is the new line to inject 201 String line = key + "=" + getFieldText(field); 202 203 try { 204 // replace old line by new one 205 mInternalTextUpdate = true; 206 207 numLines = doc.getNumberOfLines(); 208 209 IRegion info = numLines > 0 ? doc.getLineInformation(numLines - 1) : null; 210 if (info != null && info.getLength() == 0) { 211 // last line is empty. Insert right before there. 212 doc.replace(info.getOffset(), info.getLength(), line); 213 } else { 214 if (numLines > 0) { 215 String eofDelim = doc.getLineDelimiter(numLines - 1); 216 if (eofDelim == null || eofDelim.length() == 0) { 217 // The document doesn't end with a line delimiter, so add 218 // one to the line to be written. 219 line = delim + line; 220 } 221 } 222 223 int len = doc.getLength(); 224 doc.replace(len, 0, line); 225 } 226 227 allKeywords.remove(key); 228 } catch (BadLocationException e) { 229 // TODO log it 230 AdtPlugin.log(e, "Failed to append to export.properties: %s", line); 231 } finally { 232 mInternalTextUpdate = false; 233 } 234 } 235 } 236 } 237 238 /** 239 * Used when committing fields values to the model to retrieve the text 240 * associated with a field. 241 * <p/> 242 * The base method only handles {@link Text} controls. 243 * 244 * CONTRACT: Derived classes CAN use this to support their own controls. 245 * 246 * @param field A control previously registered with {@link #getNameToField()}. 247 * @return A non-null string to write to the properties files. 248 */ 249 protected String getFieldText(Control field) { 250 if (field instanceof Text) { 251 return ((Text) field).getText(); 252 } 253 return ""; 254 } 255 256 /** 257 * Called after all pages have been created, to let the parts initialize their 258 * content based on the document's model. 259 * <p/> 260 * The model should be acceded via the {@link ExportEditor}. 261 * 262 * @param editor The {@link ExportEditor} instance. 263 */ 264 public void onModelInit(ExportEditor editor) { 265 266 // Start with a set of all the possible keywords and remove those we 267 // found in the document as we read the lines. 268 HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet()); 269 270 // Parse the lines in the document for patterns "keyword=value", 271 // trimming all whitespace and discarding lines that start with # (comments) 272 // then affect to the internal fields as appropriate. 273 IDocument doc = editor.getDocument(); 274 int numLines = doc.getNumberOfLines(); 275 for (int i = 0; i < numLines; i++) { 276 try { 277 IRegion info = doc.getLineInformation(i); 278 String line = doc.get(info.getOffset(), info.getLength()); 279 line = line.trim(); 280 if (line.startsWith("#")) { //$NON-NLS-1$ 281 continue; 282 } 283 284 int pos = line.indexOf('='); 285 if (pos > 0 && pos < line.length() - 1) { 286 String key = line.substring(0, pos).trim(); 287 288 Control field = mNameToField.get(key); 289 if (field != null) { 290 String value = line.substring(pos + 1).trim(); 291 try { 292 mInternalTextUpdate = true; 293 setFieldText(field, value); 294 allKeywords.remove(key); 295 } finally { 296 mInternalTextUpdate = false; 297 } 298 } 299 } 300 301 } catch (BadLocationException e) { 302 // TODO log it 303 AdtPlugin.log(e, "Failed to set field to export.properties value"); 304 } 305 } 306 307 // Clear the text of any keyword we didn't find in the document 308 Iterator<String> iterator = allKeywords.iterator(); 309 while (iterator.hasNext()) { 310 String key = iterator.next(); 311 Control field = mNameToField.get(key); 312 if (field != null) { 313 try { 314 mInternalTextUpdate = true; 315 setFieldText(field, ""); 316 iterator.remove(); 317 } finally { 318 mInternalTextUpdate = false; 319 } 320 } 321 } 322 } 323 324 /** 325 * Used when reading the model to set the field values. 326 * <p/> 327 * The base method only handles {@link Text} controls. 328 * 329 * CONTRACT: Derived classes CAN use this to support their own controls. 330 * 331 * @param field A control previously registered with {@link #getNameToField()}. 332 * @param value A non-null string to that was read from the properties files. 333 * The value is an empty string if the property line is missing. 334 */ 335 protected void setFieldText(Control field, String value) { 336 if (field instanceof Text) { 337 ((Text) field).setText(value); 338 } 339 } 340 341 /** 342 * Called after the document model has been changed. The model should be acceded via 343 * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession) 344 * 345 * @param editor The {@link ExportEditor} instance. 346 * @param event Specification of changes applied to document. 347 */ 348 public void onModelChanged(ExportEditor editor, DocumentEvent event) { 349 // To simplify and since we don't have many fields, just reload all the values. 350 // A better way would to be to look at DocumentEvent which gives us the offset/length 351 // and text that has changed. 352 if (!mInternalTextUpdate) { 353 onModelInit(editor); 354 } 355 } 356 } 357