Home | History | Annotate | Download | only in export
      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