Friday, October 28, 2011

Form editor with XML source view

Form based editors seem to be everywhere nowadays. They provide a great way for the user to operate on complex file types like xml files. Building a forms based editor similar to the PDE Plug-in Editor is quite easy.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.


Step 1: Defining the editor

Create a new Plug-in Project called com.codeandme.multiparteditor.

I already showed how to reuse the XML source editor, so I will keep the editor definition part rather short: Define an editor and a content type in your plugin.xml. It should look like this afterwards:
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.editors">
      <editor
            class="com.codeandme.multiparteditor.SampleEditor"
            default="false"
            id="com.codeandme.editor.sampleEditor"
            name="Sample Editor">
         <contentTypeBinding
               contentTypeId="com.codeandme.contenttype.sample">
         </contentTypeBinding>
      </editor>
   </extension>
   <extension
         point="org.eclipse.core.contenttype.contentTypes">
      <content-type
            base-type="org.eclipse.core.runtime.xml"
            file-extensions="sample"
            id="com.codeandme.contenttype.sample"
            name="Sample File"
            priority="normal">
      </content-type>
   </extension>

</plugin>
Switch to the Dependencies tab and add following dependencies:
  • org.eclipse.core.resources
  • org.eclipse.core.runtime
  • org.eclipse.text
  • org.eclipse.ui
  • org.eclipse.ui.editors
  • org.eclipse.ui.forms
  • org.eclipse.ui.ide
  • org.eclipse.wst.sse.ui
  • org.eclipse.wst.xml.core

Step 2: Implementing the editor

Create a new Class com.codeandme.multiparteditor.SampleEditor
package com.codeandme.multiparteditor;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.wst.sse.ui.StructuredTextEditor;

public class SampleEditor extends FormEditor {

 private StructuredTextEditor fSourceEditor;
 private int fSourceEditorIndex;

 @Override
 public void init(final IEditorSite site, final IEditorInput input) throws PartInitException {
  super.init(site, input);

  // TODO: load your model here
 }

 @Override
 protected void addPages() {
  fSourceEditor = new StructuredTextEditor();
  fSourceEditor.setEditorPart(this);

  try {
   // add form pages
   addPage(new FirstForm(this, "firstID", "First Page"));

   // add source page
   fSourceEditorIndex = addPage(fSourceEditor, getEditorInput());
   setPageText(fSourceEditorIndex, "Source");
  } catch (final PartInitException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
 }

 @Override
 public void doSaveAs() {
  // not allowed
 }

 @Override
 public boolean isSaveAsAllowed() {
  return false;
 }

 @Override
 public void doSave(IProgressMonitor monitor) {
  throw new RuntimeException("To be implemented");
 }
}
That's all you need for your very first FormEditor.

Step 3: Add form pages

Create additional FormPages and add them to your editor. This is best done using WindowBuilder by using the New Wizard: WindowBuilder/SWT Designer/Forms/FormPage.

There are lots of tutorials out there, see here, here, or here to get you going.

Step 4: Keeping your model consistent

As we have multiple parts within our editor that modify the same source, we need some synchronization mechanism. The source editor already operates on a Document and updates its content accordingly. So our FormPages could operate on the same Document but this would result in lots of XML parsing. There are great tools for this out there, however in this example we will use another approach:

The main editor will maintain a model and inform and update an EditorPart before it is activated. That model can import and export its state to XML, which we will use to synchronize model and source editor.

All form pages should modify the model directly. When the editor page is changed, the activated page will be notified to update itself regarding new model data.

Open the SampleEditor and add following code at the end of the addPages() method:
        // add listener for changes of the document source
        getDocument().addDocumentListener(new IDocumentListener() {

            @Override
            public void documentAboutToBeChanged(final DocumentEvent event) {
                // nothing to do
            }

            @Override
            public void documentChanged(final DocumentEvent event) {
                fSourceDirty = true;
            }
        });
The listener will be notified whenever the source editor is editing the document. We use an internal flag to indicate that our form pages need an update.

Additionally add/exchange following methods:
     /** Keeps track of dirty code from source editor. */
    private boolean fSourceDirty = false;

    @Override
    public void doSave(final IProgressMonitor monitor) {
        if (getActivePage() != fSourceEditorIndex)
            updateSourceFromModel();

        fSourceEditor.doSave(monitor);
    }

    @Override
    protected void pageChange(final int newPageIndex) {
        // check for update from the source code
        if ((getActivePage() == fSourceEditorIndex) && (fSourceDirty))
            updateModelFromSource();

        // check for updates to be propagated to the source code
        if (newPageIndex == fSourceEditorIndex)
            updateSourceFromModel();

        // switch page
        super.pageChange(newPageIndex);

        // update page if needed
        final IFormPage page = getActivePageInstance();
        if (page != null) {
            // TODO update form page with new model data
            page.setFocus();
        }
    }

    private void updateModelFromSource() {
        // TODO update source code for source viewer using new model data
        fSourceDirty = false;
    }

    private void updateSourceFromModel() {
        // TODO update source page from model
        // getDocument().set("new source code");
        fSourceDirty = false;
    }

    private IDocument getDocument() {
        final IDocumentProvider provider = fSourceEditor.getDocumentProvider();
        return provider.getDocument(getEditorInput());
    }

    private IFile getFile() {
        final IEditorInput input = getEditorInput();
        if (input instanceof FileEditorInput)
            return ((FileEditorInput) input).getFile();

        return null;
    }

    private String getContent() {
        return getDocument().get();
    }


doSave() on line 5 uses the source page save routine. Therefore we need to make sure, the source code is up to date.

pageChange() on line 13 first checks if we need to either update the model from the source code or the source code from the model. This happens whenever we switch from/to the source view. After the page is switched we check whether the new page is a form page. If so, we need to update the form page (see line 28). This could be done by calling page.setFocus(); and overwrite setFocus() in the FormPage implementation.

Methods getDocument(), getFile(), getContent() are there for your convenience. You can delete the latter two if you don't need them.

10 comments:

  1. I run the project as eclipse app but the editor view is not shown and is not available in open new view dialog...

    ReplyDelete
    Replies
    1. The editor will open once you open a file called something.sample. We did not create a new File wizard here.
      Alternatively you can open the editor by right clicking any file -> Open with -> other... and select the Sample Editor

      Delete
  2. Hi Christian,

    Awesome example, It works fine directly in Eclipse I mean :

    Run --> Run configurations ----> New Configuration ----> (com.example.multiparteditor)


    So I have generated the site I mean the eclipse plugin for this example the I installed sucessfully (Help ---> Install New Software ----> ....) then I tried I always got an error : "Unsupported Content Type"

    Thanks.


    ReplyDelete
    Replies
    1. its been a while since you asked, maybe it still helps:

      add org.eclipse.wst.xml.core, to your plugin dependencies.

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. I do not know about the WindowBuilder source code. You have to start digging yourself there...

      Delete
    2. This comment has been removed by the author.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete