OSDN Git Service

ADT: Editor for export.properties in export-projects.
authorRaphael Moll <ralf@android.com>
Mon, 14 Jun 2010 22:31:21 +0000 (15:31 -0700)
committerRaphael Moll <ralf@android.com>
Mon, 14 Jun 2010 23:44:23 +0000 (16:44 -0700)
This is an initial framework that needs to be refined.

Change-Id: I2cd8a7708c30826075de076d2b5826ed8af77eb9

eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java [new file with mode: 0755]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java [new file with mode: 0755]

index 130abe8..78ebac3 100644 (file)
             name="Android Manifest Editor">
       </editor>
       <editor
+            class="com.android.ide.eclipse.adt.internal.editors.export.ExportEditor"
+            default="true"
+            filenames="export.properties"
+            icon="icons/android_file.png"
+            id="com.android.ide.eclipse.editors.export.ExportEditor"
+            name="Android Export Editor">
+      </editor>
+      <editor
             class="com.android.ide.eclipse.adt.internal.editors.resources.ResourcesEditor"
             default="false"
             extensions="xml"
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidTextEditor.java
new file mode 100755 (executable)
index 0000000..d180b5e
--- /dev/null
@@ -0,0 +1,572 @@
+/*
+ * 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;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.internal.filebuffers.SynchronizableDocument;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.jface.text.DocumentRewriteSession;
+import org.eclipse.jface.text.DocumentRewriteSessionType;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+import org.eclipse.jface.text.IDocumentListener;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+import org.eclipse.ui.editors.text.TextEditor;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.eclipse.ui.part.WorkbenchPart;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Multi-page form editor for Android text files.
+ * <p/>
+ * It is designed to work with a {@link TextEditor} that will display a text file.
+ * <br/>
+ * Derived classes must implement createFormPages to create the forms before the
+ * source editor. This can be a no-op if desired.
+ */
+public abstract class AndroidTextEditor extends FormEditor implements IResourceChangeListener {
+
+    /** Preference name for the current page of this file */
+    private static final String PREF_CURRENT_PAGE = "_current_page";
+
+    /** Id string used to create the Android SDK browser */
+    private static String BROWSER_ID = "android"; // $NON-NLS-1$
+
+    /** Page id of the XML source editor, used for switching tabs programmatically */
+    public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
+
+    /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
+    public static final int TEXT_WIDTH_HINT = 50;
+
+    /** Page index of the text editor (always the last page) */
+    private int mTextPageIndex;
+
+    /** The text editor */
+    private TextEditor mTextEditor;
+
+    /** flag set during page creation */
+    private boolean mIsCreatingPage = false;
+
+    private IDocument mDocument;
+
+    /**
+     * Creates a form editor.
+     */
+    public AndroidTextEditor() {
+        super();
+    }
+
+    // ---- Abstract Methods ----
+
+    /**
+     * Creates the various form pages.
+     * <p/>
+     * Derived classes must implement this to add their own specific tabs.
+     */
+    abstract protected void createFormPages();
+
+    /**
+     * Called by the base class {@link AndroidTextEditor} once all pages (custom form pages
+     * as well as text editor page) have been created. This give a chance to deriving
+     * classes to adjust behavior once the text page has been created.
+     */
+    protected void postCreatePages() {
+        // Nothing in the base class.
+    }
+
+    /**
+     * Subclasses should override this method to process the new text model.
+     * This is called after the document has been edited.
+     *
+     * The base implementation is empty.
+     *
+     * @param event Specification of changes applied to document.
+     */
+    protected void onDocumentChanged(DocumentEvent event) {
+        // pass
+    }
+
+    // ---- Base Class Overrides, Interfaces Implemented ----
+
+    /**
+     * Creates the pages of the multi-page editor.
+     */
+    @Override
+    protected void addPages() {
+        createAndroidPages();
+        selectDefaultPage(null /* defaultPageId */);
+    }
+
+    /**
+     * Creates the page for the Android Editors
+     */
+    protected void createAndroidPages() {
+        mIsCreatingPage = true;
+        createFormPages();
+        createTextEditor();
+        createUndoRedoActions();
+        postCreatePages();
+        mIsCreatingPage = false;
+    }
+
+    /**
+     * Returns whether the editor is currently creating its pages.
+     */
+    public boolean isCreatingPages() {
+        return mIsCreatingPage;
+    }
+
+    /**
+     * Creates undo redo actions for the editor site (so that it works for any page of this
+     * multi-page editor) by re-using the actions defined by the {@link TextEditor}
+     * (aka the XML text editor.)
+     */
+    private void createUndoRedoActions() {
+        IActionBars bars = getEditorSite().getActionBars();
+        if (bars != null) {
+            IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
+            bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
+
+            action = mTextEditor.getAction(ActionFactory.REDO.getId());
+            bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
+
+            bars.updateActionBars();
+        }
+    }
+
+    /**
+     * Selects the default active page.
+     * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
+     * find the default page in the properties of the {@link IResource} object being edited.
+     */
+    protected void selectDefaultPage(String defaultPageId) {
+        if (defaultPageId == null) {
+            if (getEditorInput() instanceof IFileEditorInput) {
+                IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+                QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+                        getClass().getSimpleName() + PREF_CURRENT_PAGE);
+                String pageId;
+                try {
+                    pageId = file.getPersistentProperty(qname);
+                    if (pageId != null) {
+                        defaultPageId = pageId;
+                    }
+                } catch (CoreException e) {
+                    // ignored
+                }
+            }
+        }
+
+        if (defaultPageId != null) {
+            try {
+                setActivePage(Integer.parseInt(defaultPageId));
+            } catch (Exception e) {
+                // We can get NumberFormatException from parseInt but also
+                // AssertionError from setActivePage when the index is out of bounds.
+                // Generally speaking we just want to ignore any exception and fall back on the
+                // first page rather than crash the editor load. Logging the error is enough.
+                AdtPlugin.log(e, "Selecting page '%s' in AndroidXmlEditor failed", defaultPageId);
+            }
+        }
+    }
+
+    /**
+     * Removes all the pages from the editor.
+     */
+    protected void removePages() {
+        int count = getPageCount();
+        for (int i = count - 1 ; i >= 0 ; i--) {
+            removePage(i);
+        }
+    }
+
+    /**
+     * Overrides the parent's setActivePage to be able to switch to the xml editor.
+     *
+     * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
+     * This is needed because the editor doesn't actually derive from IFormPage and thus
+     * doesn't have the get-by-page-id method. In this case, the method returns null since
+     * IEditorPart does not implement IFormPage.
+     */
+    @Override
+    public IFormPage setActivePage(String pageId) {
+        if (pageId.equals(TEXT_EDITOR_ID)) {
+            super.setActivePage(mTextPageIndex);
+            return null;
+        } else {
+            return super.setActivePage(pageId);
+        }
+    }
+
+
+    /**
+     * Notifies this multi-page editor that the page with the given id has been
+     * activated. This method is called when the user selects a different tab.
+     *
+     * @see MultiPageEditorPart#pageChange(int)
+     */
+    @Override
+    protected void pageChange(int newPageIndex) {
+        super.pageChange(newPageIndex);
+
+        // Do not record page changes during creation of pages
+        if (mIsCreatingPage) {
+            return;
+        }
+
+        if (getEditorInput() instanceof IFileEditorInput) {
+            IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+            QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+                    getClass().getSimpleName() + PREF_CURRENT_PAGE);
+            try {
+                file.setPersistentProperty(qname, Integer.toString(newPageIndex));
+            } catch (CoreException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Notifies this listener that some resource changes
+     * are happening, or have already happened.
+     *
+     * Closes all project files on project close.
+     * @see IResourceChangeListener
+     */
+    public void resourceChanged(final IResourceChangeEvent event) {
+        if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
+            Display.getDefault().asyncExec(new Runnable() {
+                public void run() {
+                    IWorkbenchPage[] pages = getSite().getWorkbenchWindow()
+                            .getPages();
+                    for (int i = 0; i < pages.length; i++) {
+                        if (((FileEditorInput)mTextEditor.getEditorInput())
+                                .getFile().getProject().equals(
+                                        event.getResource())) {
+                            IEditorPart editorPart = pages[i].findEditor(mTextEditor
+                                    .getEditorInput());
+                            pages[i].closeEditor(editorPart, true);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Initializes the editor part with a site and input.
+     * <p/>
+     * Checks that the input is an instance of {@link IFileEditorInput}.
+     *
+     * @see FormEditor
+     */
+    @Override
+    public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
+        if (!(editorInput instanceof IFileEditorInput))
+            throw new PartInitException("Invalid Input: Must be IFileEditorInput");
+        super.init(site, editorInput);
+    }
+
+    /**
+     * Removes attached listeners.
+     *
+     * @see WorkbenchPart
+     */
+    @Override
+    public void dispose() {
+        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
+
+        super.dispose();
+    }
+
+    /**
+     * Commit all dirty pages then saves the contents of the text editor.
+     * <p/>
+     * This works by committing all data to the XML model and then
+     * asking the Structured XML Editor to save the XML.
+     *
+     * @see IEditorPart
+     */
+    @Override
+    public void doSave(IProgressMonitor monitor) {
+        commitPages(true /* onSave */);
+
+        // The actual "save" operation is done by the Structured XML Editor
+        getEditor(mTextPageIndex).doSave(monitor);
+    }
+
+    /* (non-Javadoc)
+     * Saves the contents of this editor to another object.
+     * <p>
+     * Subclasses must override this method to implement the open-save-close lifecycle
+     * for an editor.  For greater details, see <code>IEditorPart</code>
+     * </p>
+     *
+     * @see IEditorPart
+     */
+    @Override
+    public void doSaveAs() {
+        commitPages(true /* onSave */);
+
+        IEditorPart editor = getEditor(mTextPageIndex);
+        editor.doSaveAs();
+        setPageText(mTextPageIndex, editor.getTitle());
+        setInput(editor.getEditorInput());
+    }
+
+    /**
+     * Commits all dirty pages in the editor. This method should
+     * be called as a first step of a 'save' operation.
+     * <p/>
+     * This is the same implementation as in {@link FormEditor}
+     * except it fixes two bugs: a cast to IFormPage is done
+     * from page.get(i) <em>before</em> being tested with instanceof.
+     * Another bug is that the last page might be a null pointer.
+     * <p/>
+     * The incorrect casting makes the original implementation crash due
+     * to our {@link StructuredTextEditor} not being an {@link IFormPage}
+     * so we have to override and duplicate to fix it.
+     *
+     * @param onSave <code>true</code> if commit is performed as part
+     * of the 'save' operation, <code>false</code> otherwise.
+     * @since 3.3
+     */
+    @Override
+    public void commitPages(boolean onSave) {
+        if (pages != null) {
+            for (int i = 0; i < pages.size(); i++) {
+                Object page = pages.get(i);
+                if (page != null && page instanceof IFormPage) {
+                    IFormPage form_page = (IFormPage) page;
+                    IManagedForm managed_form = form_page.getManagedForm();
+                    if (managed_form != null && managed_form.isDirty()) {
+                        managed_form.commit(onSave);
+                    }
+                }
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * Returns whether the "save as" operation is supported by this editor.
+     * <p>
+     * Subclasses must override this method to implement the open-save-close lifecycle
+     * for an editor.  For greater details, see <code>IEditorPart</code>
+     * </p>
+     *
+     * @see IEditorPart
+     */
+    @Override
+    public boolean isSaveAsAllowed() {
+        return false;
+    }
+
+    // ---- Local methods ----
+
+
+    /**
+     * Helper method that creates a new hyper-link Listener.
+     * Used by derived classes which need active links in {@link FormText}.
+     * <p/>
+     * This link listener handles two kinds of URLs:
+     * <ul>
+     * <li> Links starting with "http" are simply sent to a local browser.
+     * <li> Links starting with "file:/" are simply sent to a local browser.
+     * <li> Links starting with "page:" are expected to be an editor page id to switch to.
+     * <li> Other links are ignored.
+     * </ul>
+     *
+     * @return A new hyper-link listener for FormText to use.
+     */
+    public final IHyperlinkListener createHyperlinkListener() {
+        return new HyperlinkAdapter() {
+            /**
+             * Switch to the page corresponding to the link that has just been clicked.
+             * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
+             */
+            @Override
+            public void linkActivated(HyperlinkEvent e) {
+                super.linkActivated(e);
+                String link = e.data.toString();
+                if (link.startsWith("http") ||          //$NON-NLS-1$
+                        link.startsWith("file:/")) {    //$NON-NLS-1$
+                    openLinkInBrowser(link);
+                } else if (link.startsWith("page:")) {  //$NON-NLS-1$
+                    // Switch to an internal page
+                    setActivePage(link.substring(5 /* strlen("page:") */));
+                }
+            }
+        };
+    }
+
+    /**
+     * Open the http link into a browser
+     *
+     * @param link The URL to open in a browser
+     */
+    private void openLinkInBrowser(String link) {
+        try {
+            IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
+            wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
+        } catch (PartInitException e1) {
+            // pass
+        } catch (MalformedURLException e1) {
+            // pass
+        }
+    }
+
+    /**
+     * Creates the XML source editor.
+     * <p/>
+     * Memorizes the index page of the source editor (it's always the last page, but the number
+     * of pages before can change.)
+     * <br/>
+     * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
+     * Finally triggers modelChanged() on the model listener -- derived classes can use this
+     * to initialize the model the first time.
+     * <p/>
+     * Called only once <em>after</em> createFormPages.
+     */
+    private void createTextEditor() {
+        try {
+            mTextEditor = new TextEditor();
+            int index = addPage(mTextEditor, getEditorInput());
+            mTextPageIndex = index;
+            setPageText(index, mTextEditor.getTitle());
+
+            IDocumentProvider provider = mTextEditor.getDocumentProvider();
+            mDocument = provider.getDocument(getEditorInput());
+
+            mDocument.addDocumentListener(new IDocumentListener() {
+                public void documentChanged(DocumentEvent event) {
+                    onDocumentChanged(event);
+                }
+
+                public void documentAboutToBeChanged(DocumentEvent event) {
+                    // ignore
+                }
+            });
+
+
+        } catch (PartInitException e) {
+            ErrorDialog.openError(getSite().getShell(),
+                    "Android Text Editor Error", null, e.getStatus());
+        }
+    }
+
+    /**
+     * Gives access to the {@link IDocument} from the {@link TextEditor}, corresponding to
+     * the current file input.
+     * <p/>
+     * All edits should be wrapped in a {@link #wrapRewriteSession(Runnable)}.
+     * The actual document instance is a {@link SynchronizableDocument}, which creates a lock
+     * around read/set operations. The base API provided by {@link IDocument} provides ways to
+     * manipulate the document line per line or as a bulk.
+     */
+    public IDocument getDocument() {
+        return mDocument;
+    }
+
+    /**
+     * Returns the {@link IProject} for the edited file.
+     */
+    public IProject getProject() {
+        if (mTextEditor != null) {
+            IEditorInput input = mTextEditor.getEditorInput();
+            if (input instanceof FileEditorInput) {
+                FileEditorInput fileInput = (FileEditorInput)input;
+                IFile inputFile = fileInput.getFile();
+
+                if (inputFile != null) {
+                    return inputFile.getProject();
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Runs the given operation in the context of a document RewriteSession.
+     * Takes care of properly starting and stopping the operation.
+     * <p/>
+     * The operation itself should just access {@link #getDocument()} and use the
+     * normal document's API to manipulate it.
+     *
+     * @see #getDocument()
+     */
+    public void wrapRewriteSession(Runnable operation) {
+        if (mDocument instanceof IDocumentExtension4) {
+            IDocumentExtension4 doc4 = (IDocumentExtension4) mDocument;
+
+            DocumentRewriteSession session = null;
+            try {
+                session = doc4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
+
+                operation.run();
+            } catch(IllegalStateException e) {
+                AdtPlugin.log(e, "wrapRewriteSession failed");
+                e.printStackTrace();
+            } finally {
+                if (session != null) {
+                    doc4.stopRewriteSession(session);
+                }
+            }
+
+        } else {
+            // Not an IDocumentExtension4? Unlikely. Try the operation anyway.
+            operation.run();
+        }
+    }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/AbstractPropertiesFieldsPart.java
new file mode 100755 (executable)
index 0000000..06169d2
--- /dev/null
@@ -0,0 +1,351 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportEditor.java
new file mode 100755 (executable)
index 0000000..50d9fd8
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * 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.AndroidConstants;
+import com.android.ide.eclipse.adt.internal.editors.AndroidTextEditor;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Multi-page form editor for export.properties in Export Projects.
+ */
+public class ExportEditor extends AndroidTextEditor {
+
+    public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".text.ExportEditor"; //$NON-NLS-1$
+
+    private ExportPropertiesPage mExportPropsPage;
+
+    /**
+     * Creates the form editor for resources XML files.
+     */
+    public ExportEditor() {
+        super();
+    }
+
+    // ---- Base Class Overrides ----
+
+    /**
+     * Returns whether the "save as" operation is supported by this editor.
+     * <p/>
+     * Save-As is a valid operation for the ManifestEditor since it acts on a
+     * single source file.
+     *
+     * @see IEditorPart
+     */
+    @Override
+    public boolean isSaveAsAllowed() {
+        return true;
+    }
+
+    /**
+     * Create the various form pages.
+     */
+    @Override
+    protected void createFormPages() {
+        try {
+            mExportPropsPage = new ExportPropertiesPage(this);
+            addPage(mExportPropsPage);
+        } catch (PartInitException e) {
+            AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+        }
+
+    }
+
+    /* (non-java doc)
+     * Change the tab/title name to include the project name.
+     */
+    @Override
+    protected void setInput(IEditorInput input) {
+        super.setInput(input);
+        if (input instanceof FileEditorInput) {
+            FileEditorInput fileInput = (FileEditorInput) input;
+            IFile file = fileInput.getFile();
+            setPartName(String.format("%1$s", file.getName()));
+        }
+    }
+
+    @Override
+    protected void postCreatePages() {
+        super.postCreatePages();
+        mExportPropsPage.onModelInit();
+    }
+
+    /**
+     * Indicates changes were made to the document.
+     *
+     * @param event Specification of changes applied to document.
+     */
+    @Override
+    protected void onDocumentChanged(DocumentEvent event) {
+        super.onDocumentChanged(event);
+        mExportPropsPage.onModelChanged(event);
+    }
+
+    // ---- Local Methods ----
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportFieldsPart.java
new file mode 100755 (executable)
index 0000000..eff3e48
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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 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;
+
+/**
+ * Section part for editing the properties in an Export editor.
+ */
+final class ExportFieldsPart extends AbstractPropertiesFieldsPart {
+
+    public ExportFieldsPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
+        super(body, toolkit, editor);
+        Section section = getSection();
+
+        section.setText("Export Properties");
+        section.setDescription("Properties of export.properties:");
+
+        Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+        createLabel(table, toolkit,
+                "Available Properties", //label
+                "List of properties you can edit in export.properties");  //tooltip
+
+        Text packageField = createLabelAndText(table, toolkit,
+                "Package", //label,
+                "", //$NON-NLS-1$ value,
+                "TODO tooltip for Package");  //tooltip
+
+        Text projectsField = createLabelAndText(table, toolkit,
+                "Projects", //label,
+                "", //$NON-NLS-1$ value,
+                "TODO tooltip for Projects");  //tooltip
+
+        Text versionCodeField = createLabelAndText(table, toolkit,
+                "Version Code", //label,
+                "", //$NON-NLS-1$ value,
+                "TODO tooltip for Version Code");  //tooltip
+
+        Text keyStoreField = createLabelAndText(table, toolkit,
+                "Key Store", //label,
+                "", //$NON-NLS-1$ value,
+                "TODO tooltip for Key Store");  //tooltip
+
+        Text keyAliasField = createLabelAndText(table, toolkit,
+                "Key Alias", //label,
+                "", //$NON-NLS-1$ value,
+                "TODO tooltip for Key Alias");  //tooltip
+
+        // Associate each field with the keyword in the properties files.
+        // TODO there's probably some constant to reuse here.
+        HashMap<String, Control> map = getNameToField();
+        map.put("package", packageField);              //$NON-NLS-1$
+        map.put("projects", projectsField);            //$NON-NLS-1$
+        map.put("versionCode", versionCodeField);      //$NON-NLS-1$
+        map.put("_key.store", keyStoreField);          //$NON-NLS-1$
+        map.put("_key.alias", keyAliasField);          //$NON-NLS-1$
+
+        addModifyListenerToFields();
+    }
+
+    @Override
+    protected void setFieldModifyListener(Control field, ModifyListener markDirtyListener) {
+        super.setFieldModifyListener(field, markDirtyListener);
+        // TODO override for custom controls
+    }
+
+    @Override
+    protected String getFieldText(Control field) {
+        // TODO override for custom controls
+        return super.getFieldText(field);
+    }
+
+    @Override
+    protected void setFieldText(Control field, String value) {
+        // TODO override for custom controls
+        super.setFieldText(field, value);
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportLinksPart.java
new file mode 100755 (executable)
index 0000000..31ea988
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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 org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.MessageBox;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Links section part for export properties page.
+ * Displays some help and some links/actions for the user to use.
+ */
+final class ExportLinksPart extends ManifestSectionPart {
+
+    private FormText mFormText;
+
+    public ExportLinksPart(Composite body, FormToolkit toolkit, ExportEditor editor) {
+        super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+        Section section = getSection();
+        section.setText("Links");
+        section.setDescription("TODO SOME TEXT HERE. You can also edit the XML directly.");
+
+        final Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+        StringBuffer buf = new StringBuffer();
+        buf.append("<form>"); // $NON-NLS-1$
+
+        buf.append("<li style=\"image\" value=\"android_img\"><a href=\"action_dosomething\">");
+        buf.append("TODO Custom Action");
+        buf.append("</a>"); //$NON-NLS-1$
+        buf.append(": blah blah do something (like build/export).");
+        buf.append("</li>"); //$NON-NLS-1$
+
+        buf.append(String.format("<li style=\"image\" value=\"android_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$
+                ExportEditor.TEXT_EDITOR_ID));
+        buf.append("XML Source");
+        buf.append("</a>"); //$NON-NLS-1$
+        buf.append(": Directly edit the AndroidManifest.xml file.");
+        buf.append("</li>"); //$NON-NLS-1$
+
+        buf.append("<li style=\"image\" value=\"android_img\">"); // $NON-NLS-1$
+        buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); // $NON-NLS-1$
+        buf.append("</li>"); //$NON-NLS-1$
+        buf.append("</form>"); //$NON-NLS-1$
+
+        mFormText = createFormText(table, toolkit, true, buf.toString(),
+                false /* setupLayoutData */);
+
+        Image androidLogo = AdtPlugin.getAndroidLogo();
+        mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$
+
+        // Listener for default actions (page change, URL web browser)
+        mFormText.addHyperlinkListener(editor.createHyperlinkListener());
+
+        mFormText.addHyperlinkListener(new IHyperlinkListener() {
+            public void linkExited(HyperlinkEvent e) {
+                // pass
+            }
+
+            public void linkEntered(HyperlinkEvent e) {
+                // pass
+            }
+
+            public void linkActivated(HyperlinkEvent e) {
+                String link = e.data.toString();
+                if ("action_dosomething".equals(link)) {
+                    MessageBox mb = new MessageBox(table.getShell(), SWT.OK);
+                    mb.setText("Custom Action Invoked");
+                    mb.open();
+                }
+            }
+        });
+    }
+
+    /**
+     * 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) {
+        // pass
+    }
+
+    /**
+     * 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) {
+        // pass
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/export/ExportPropertiesPage.java
new file mode 100755 (executable)
index 0000000..f3db5ee
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ColumnLayout;
+import org.eclipse.ui.forms.widgets.ColumnLayoutData;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+
+/**
+ * Page for export properties, used by {@link ExportEditor}.
+ * It displays a part to edit the properties and another part
+ * to provide some links and actions.
+ */
+public final class ExportPropertiesPage extends FormPage {
+
+    /** Page id used for switching tabs programmatically */
+    final static String PAGE_ID = "export_prop_page"; //$NON-NLS-1$
+
+    /** Container editor */
+    ExportEditor mEditor;
+    /** Export fields part */
+    private ExportFieldsPart mFieldsPart;
+    /** Export links part */
+    private ExportLinksPart mLinksPart;
+
+    public ExportPropertiesPage(ExportEditor editor) {
+        super(editor, PAGE_ID, "Export Properties");  // tab's label, user visible, keep it short
+        mEditor = editor;
+    }
+
+    /**
+     * Creates the content in the form hosted in this page.
+     *
+     * @param managedForm the form hosted in this page.
+     */
+    @Override
+    protected void createFormContent(IManagedForm managedForm) {
+        super.createFormContent(managedForm);
+        ScrolledForm form = managedForm.getForm();
+        form.setText("Android Export Properties");
+        form.setImage(AdtPlugin.getAndroidLogo());
+
+        Composite body = form.getBody();
+        FormToolkit toolkit = managedForm.getToolkit();
+
+        body.setLayout(new ColumnLayout());
+
+        mFieldsPart = new ExportFieldsPart(body, toolkit, mEditor);
+        mFieldsPart.getSection().setLayoutData(new ColumnLayoutData());
+        managedForm.addPart(mFieldsPart);
+
+        mLinksPart = new ExportLinksPart(body, toolkit, mEditor);
+        mLinksPart.getSection().setLayoutData(new ColumnLayoutData());
+        managedForm.addPart(mLinksPart);
+
+        mFieldsPart.onModelInit(mEditor);
+        mLinksPart.onModelInit(mEditor);
+    }
+
+    /**
+     * 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}.
+     */
+    public void onModelInit() {
+        if (mFieldsPart != null) {
+            mFieldsPart.onModelInit(mEditor);
+        }
+
+        if (mLinksPart != null) {
+            mLinksPart.onModelInit(mEditor);
+        }
+    }
+
+    /**
+     * Called after the document model has been changed. The model should be acceded via
+     * the {@link ExportEditor}.
+     *
+     * @param event Specification of changes applied to document.
+     */
+    public void onModelChanged(DocumentEvent event) {
+        if (mFieldsPart != null) {
+            mFieldsPart.onModelChanged(mEditor, event);
+        }
+
+        if (mLinksPart != null) {
+            mLinksPart.onModelChanged(mEditor, event);
+        }
+    }
+}