blob: bc3051e88ecf3c796bf49360e546fb6599c0bd49 [file] [log] [blame]
/*
* 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.SdkConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
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;
import java.util.Iterator;
/**
* 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() {
@Override
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() {
@Override
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 != null && 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
Iterator<String> iterator = allKeywords.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
Control field = mNameToField.get(key);
if (field != null) {
try {
mInternalTextUpdate = true;
setFieldText(field, "");
iterator.remove();
} 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);
}
}
}