/* * Copyright (C) 2010 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.editors.export; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; import com.android.sdklib.SdkConstants; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.Section; import java.util.HashMap; import java.util.HashSet; /** * Section part for editing fields of a properties file in an Export editor. * <p/> * This base class is intended to be derived and customized. */ abstract class AbstractPropertiesFieldsPart extends ManifestSectionPart { private final HashMap<String, Control> mNameToField = new HashMap<String, Control>(); private ExportEditor mEditor; private boolean mInternalTextUpdate = false; public AbstractPropertiesFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) { super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); mEditor = editor; } protected HashMap<String, Control> getNameToField() { return mNameToField; } protected ExportEditor getEditor() { return mEditor; } protected void setInternalTextUpdate(boolean internalTextUpdate) { mInternalTextUpdate = internalTextUpdate; } protected boolean isInternalTextUpdate() { return mInternalTextUpdate; } /** * Adds a modify listener to every text field that will mark the part as dirty. * * CONTRACT: Derived classes MUST call this at the end of their constructor. * * @see #setFieldModifyListener(Control, ModifyListener) */ protected void addModifyListenerToFields() { ModifyListener markDirtyListener = new ModifyListener() { public void modifyText(ModifyEvent e) { // Mark the part as dirty if a field has been changed. // This will force a commit() operation to store the data in the model. if (!mInternalTextUpdate) { markDirty(); } } }; for (Control field : mNameToField.values()) { setFieldModifyListener(field, markDirtyListener); } } /** * Sets a listener that will mark the part as dirty when the control is modified. * The base method only handles {@link Text} fields. * * CONTRACT: Derived classes CAN use this to add a listener to their own controls. * The listener must call {@link #markDirty()} when the control is modified by the user. * * @param field A control previously registered with {@link #getNameToField()}. * @param markDirtyListener A {@link ModifyListener} that invokes {@link #markDirty()}. * * @see #isInternalTextUpdate() */ protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) { if (field instanceof Text) { ((Text) field).addModifyListener(markDirtyListener); } } /** * Updates the model based on the content of fields. This is invoked when a field * has marked the document as dirty. * * CONTRACT: Derived classes do not need to override this. */ @Override public void commit(boolean onSave) { // We didn't store any information indicating which field was dirty (we could). // Since there are not many fields, just update all the document lines that // match our field keywords. if (isDirty()) { mEditor.wrapRewriteSession(new Runnable() { public void run() { saveFieldsToModel(); } }); } super.commit(onSave); } private void saveFieldsToModel() { // Get a list of all keywords to process. Go thru the document, replacing in-place // the ones we can find and remove them from this set. This will leave the list // of new keywords to add at the end of the document. HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet()); IDocument doc = mEditor.getDocument(); int numLines = doc.getNumberOfLines(); String delim = null; try { delim = numLines > 0 ? doc.getLineDelimiter(0) : null; } catch (BadLocationException e1) { // ignore } if (delim == null || delim.length() == 0) { delim = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS ? "\r\n" : "\n"; //$NON-NLS-1$ //$NON-NLS-2# } for (int i = 0; i < numLines; i++) { try { IRegion info = doc.getLineInformation(i); String line = doc.get(info.getOffset(), info.getLength()); line = line.trim(); if (line.startsWith("#")) { //$NON-NLS-1$ continue; } int pos = line.indexOf('='); if (pos > 0 && pos < line.length() - 1) { String key = line.substring(0, pos).trim(); Control field = mNameToField.get(key); if (field != null) { // This is the new line to inject line = key + "=" + getFieldText(field); try { // replace old line by new one. This doesn't change the // line delimiter. mInternalTextUpdate = true; doc.replace(info.getOffset(), info.getLength(), line); allKeywords.remove(key); } finally { mInternalTextUpdate = false; } } } } catch (BadLocationException e) { // TODO log it AdtPlugin.log(e, "Failed to replace in export.properties"); } } for (String key : allKeywords) { Control field = mNameToField.get(key); if (field != null) { // This is the new line to inject String line = key + "=" + getFieldText(field); try { // replace old line by new one mInternalTextUpdate = true; numLines = doc.getNumberOfLines(); IRegion info = numLines > 0 ? doc.getLineInformation(numLines - 1) : null; if (info.getLength() == 0) { // last line is empty. Insert right before there. doc.replace(info.getOffset(), info.getLength(), line); } else { if (numLines > 0) { String eofDelim = doc.getLineDelimiter(numLines - 1); if (eofDelim == null || eofDelim.length() == 0) { // The document doesn't end with a line delimiter, so add // one to the line to be written. line = delim + line; } } int len = doc.getLength(); doc.replace(len, 0, line); } allKeywords.remove(key); } catch (BadLocationException e) { // TODO log it AdtPlugin.log(e, "Failed to append to export.properties: %s", line); } finally { mInternalTextUpdate = false; } } } } /** * Used when committing fields values to the model to retrieve the text * associated with a field. * <p/> * The base method only handles {@link Text} controls. * * CONTRACT: Derived classes CAN use this to support their own controls. * * @param field A control previously registered with {@link #getNameToField()}. * @return A non-null string to write to the properties files. */ protected String getFieldText(Control field) { if (field instanceof Text) { return ((Text) field).getText(); } return ""; } /** * Called after all pages have been created, to let the parts initialize their * content based on the document's model. * <p/> * The model should be acceded via the {@link ExportEditor}. * * @param editor The {@link ExportEditor} instance. */ public void onModelInit(ExportEditor editor) { // Start with a set of all the possible keywords and remove those we // found in the document as we read the lines. HashSet<String> allKeywords = new HashSet<String>(mNameToField.keySet()); // Parse the lines in the document for patterns "keyword=value", // trimming all whitespace and discarding lines that start with # (comments) // then affect to the internal fields as appropriate. IDocument doc = editor.getDocument(); int numLines = doc.getNumberOfLines(); for (int i = 0; i < numLines; i++) { try { IRegion info = doc.getLineInformation(i); String line = doc.get(info.getOffset(), info.getLength()); line = line.trim(); if (line.startsWith("#")) { //$NON-NLS-1$ continue; } int pos = line.indexOf('='); if (pos > 0 && pos < line.length() - 1) { String key = line.substring(0, pos).trim(); Control field = mNameToField.get(key); if (field != null) { String value = line.substring(pos + 1).trim(); try { mInternalTextUpdate = true; setFieldText(field, value); allKeywords.remove(key); } finally { mInternalTextUpdate = false; } } } } catch (BadLocationException e) { // TODO log it AdtPlugin.log(e, "Failed to set field to export.properties value"); } } // Clear the text of any keyword we didn't find in the document for (String key : allKeywords) { Control field = mNameToField.get(key); if (field != null) { try { mInternalTextUpdate = true; setFieldText(field, ""); allKeywords.remove(key); } finally { mInternalTextUpdate = false; } } } } /** * Used when reading the model to set the field values. * <p/> * The base method only handles {@link Text} controls. * * CONTRACT: Derived classes CAN use this to support their own controls. * * @param field A control previously registered with {@link #getNameToField()}. * @param value A non-null string to that was read from the properties files. * The value is an empty string if the property line is missing. */ protected void setFieldText(Control field, String value) { if (field instanceof Text) { ((Text) field).setText(value); } } /** * Called after the document model has been changed. The model should be acceded via * the {@link ExportEditor} (e.g. getDocument, wrapRewriteSession) * * @param editor The {@link ExportEditor} instance. * @param event Specification of changes applied to document. */ public void onModelChanged(ExportEditor editor, DocumentEvent event) { // To simplify and since we don't have many fields, just reload all the values. // A better way would to be to look at DocumentEvent which gives us the offset/length // and text that has changed. if (!mInternalTextUpdate) { onModelInit(editor); } } }