blob: 6fd548ce7c0f6911001f68ac37bd84ee893d2fae [file] [log] [blame]
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Integrating EMF and GMF Generated Editors</title>
<meta http-equiv="Content-Type"
content="text/html; charset=windows-1252">
<link href="../article.css" type="text/css" rel="stylesheet">
</head>
<body>
<h1>Integrating EMF and GMF Generated Editors</h1>
<div class="summary">
<h2>Summary</h2>
<p>This article provides a walk-through example how to combine the
editor plug-ins generated by the Eclipse Modeling Framework (EMF) and
the Graphical Modeling Framework (GMF) to create an integrated editor
that supports graphical and list- oder tree-based editing of the same
information. The approach outlined in this article can easily be used as
a starting point for the implementation of arbitrary multi-page editor
solutions that contain GMF-based graphical editors.</p>
<div class="author">by Volker Wegert and Alex Shatalin</div>
<div class="copyright">Copyright &copy; 2008 Volker Wegert and
Alex Shatalin</div>
<div class="date">March 20, 2008</div>
</div>
<div class="content">
<h2>Table of Contents</h2>
<ol>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#setting_the_stage">Setting the Stage</a></li>
<li><a href="#upgrading_the_editing_domain">Upgrading the
Editing Domain</a></li>
<li><a href="#extending_the_editor_input">Extending the Editor
Input</a></li>
<li><a href="#dissecting_the_emf_editor">Dissecting the EMF
Editor</a></li>
<li><a href="#selection_handling">Selection Handling</a></li>
<li><a href="#combining_the_editors">Combining the Editors</a></li>
<li><a href="#taming_the_properties">Taming the Properties</a></li>
<li><a href="#completing_menus_and_toolbars">Completing Menus
and Toolbars</a></li>
<li><a href="#fixing_edit_menu_actions">Fixing Edit Menu
Actions</a></li>
<li><a href="#creating_the_diagram">Creating the Diagram</a></li>
<li><a href="#fixing_the_resource_navigator">Fixing the
Resource Navigator</a></li>
<li><a href="#rcp">Want some RCP with that?</a></li>
<li><a href="#miscellaneous_notes">Miscellaneous Notes</a></li>
<li><a href="#conclusion">Conclusion</a></li>
<li><a href="#acknowledgements">Acknowledgements</a></li>
</ol>
<h2><a name="introduction">Introduction</a></h2>
<p>Both the Eclipse Modeling Framework (<a
href="http://www.eclipse.org/emf">EMF</a>) and the Graphical Modeling
Framework (<a href="http://www.eclipse.org/gmf">GMF</a>) are capable of
generating editor plug-ins. However, there are many use cases where it's
not a question of <i>traditional widget-based editor or graphical
editor</i>&mdash;a joint solution that provides both widget-based and
graphical editing capabilities is called for. Unfortunately, it's not a
trivial task to combine the generated editors. This article walks you
through the steps required to create an integrated editor. Although we
start off from scratch, you should have some basic knowledge about how
both EMF and GMF work.</p>
<p>This article uses a simple example model&mdash;about the smallest
working example possible&mdash;shown in the following image.</p>
<p><img src="screenshots/EcoreModel.png" alt="Topic Map Ecore Model" /></p>
<p>The main object stored in a file is a topic map that consists of
an arbitrary number of topics and associations. Both topics and
associations are labeled. An association always connects two topics.</p>
<p>If you just want to see the combined editor in action, you can
grab an <a href="files/projects-final-state.zip">archive</a> containing
the result of the actions described below.</p>
<h2><a name="setting_the_stage">Setting the Stage</a></h2>
<p>Before we can try to integrate the generated editors, we first
need to setup some playground projects. This is covered by the
documentation provided for EMF and GMF, so let's not get lost in the
details. You can use the <a href="files/projects-initial-state.zip">archived
example projects</a> provided with this article, they contain the models
used in this article as well as the generated plug-ins in an "untainted"
state. If you prefer to setup your environment manually, just follow
this checklist.</p>
<ol>
<li>Create an empty EMF project and name it <tt>org.example.emfgmf</tt>.</li>
<li>Create a new Ecore model as shown above or use the <a
href="files/example-models.zip">archive containing the models</a> used
in this article. This archive also contains the GMF models to save you
some work.</li>
<li>Create a generator model for the Ecore model. Remember to
adjust the package properties.</li>
<li>Generate the model, edit and editor plug-ins. There's no need
to generate the test classes, but it doesn't hurt either.</li>
<li>Create a Graphical Definition and a Tooling Definition using
the wizard.</li>
<li>Create a Mapping Model using the wizard. You'll have to adapt
the label mappings if you use the sample model provided since the
wizard doesn't generate sensible values here.</li>
<li>Create a GMF generator model. <b>Change the property <i>Same
File for Diagram And Model</i> to <tt>true</tt> and change diagram file
extension to match the domain file extension</b> - if you skip this, things
will get a bit more complicated later.</li>
<li>Generate the diagram editor.</li>
</ol>
<blockquote>
<p><img src="images/tryit.gif" width="61" height="13" alt="Try it!">
You can now launch an Eclipse runtime with the generated plug-ins.
Create a project and some topic map to play with. Try editing your topic
map with both the tree-like EMF editor and the GMF-based diagram editor
and watch the results.</p>
</blockquote>
<p><img src="screenshots/GeneratedEMFEditor.png"
alt="Editor generated by the EMF" /></p>
<p><img src="screenshots/GeneratedGMFEditor.png"
alt="Editor generated by the GMF" /></p>
<p>Now, let's put these two editors together...</p>
<h2><a name="upgrading_the_editing_domain">Upgrading the
Editing Domain</a></h2>
<p>Both the EMF-based and the GMF-based editor use an <tt>EditingDomain</tt>
to handle the model objects generated from the Ecore model. To allow for
simultaneous modifications of the same model instances in the two editor
areas, we have to modify generated code to share the same <tt>EditingDomain</tt>
instance across both the EMF-based and the GMF-based editor.</p>
<p>The GMF-based diagram editor uses a <tt>TransactionalEditingDomain</tt>
while the EMF editor "only" requires an <tt>AdapterFactoryEditingDomain</tt>.
In order to combine the editors, we have to "upgrade" the EMF editor to
create a <tt>TransactionalEditingDomain</tt> that can then be passed
along to the diagram editor. Open the <tt>TopicmapEditor</tt> source and
adapt the generated implementation of <tt>initializeEditingDomain()</tt>
as shown in the following code:
<pre>
protected void initializeEditingDomain() {
// Create an adapter factory that yields item providers.
//
adapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
adapterFactory.addAdapterFactory(new ResourceItemProviderAdapterFactory());
adapterFactory.addAdapterFactory(new TopicmapItemProviderAdapterFactory());
adapterFactory.addAdapterFactory(new ReflectiveItemProviderAdapterFactory());
<b>// Create a transactional editing domain
//
TransactionalEditingDomain domain = TransactionalEditingDomain.Factory.INSTANCE.createEditingDomain();
domain.setID("org.example.emfgmf.diagram.EditingDomain");</b>
// Add a listener to set the most recent command's affected objects to be the selection of the viewer with focus.
//
<b>domain.getCommandStack()</b>.addCommandStackListener(
new CommandStackListener() {
public void commandStackChanged(final EventObject event) {
getContainer().getDisplay().asyncExec
(new Runnable() {
public void run() {
firePropertyChange(IEditorPart.PROP_DIRTY);
// Try to select the affected objects.
//
Command mostRecentCommand = ((CommandStack) event.getSource()).getMostRecentCommand();
if (mostRecentCommand != null) {
setSelectionToViewer(mostRecentCommand.getAffectedObjects());
}
if (propertySheetPage != null &amp;&amp; !propertySheetPage.getControl().isDisposed()) {
propertySheetPage.refresh();
}
}
});
}
});
<b>editingDomain = (AdapterFactoryEditingDomain) domain;</b>
}</pre>
<blockquote>
<p><img src="images/tip.gif" width="62" height="13" alt="Tip">
Don't forget to set the <tt>@generated</tt> tag to an arbitrary value to
prevent your changes from being overwritten.</p>
</blockquote>
<p>Also, in order for this to work, the <tt>org.example.emfgmf.editor</tt>
plugin needs another dependency to <tt>org.eclipse.emf.transaction</tt>.</p>
<p>Make sure that the ID used to initialize the editing domain
matches the editing domain ID specified in the GMF generator model
(select the <i>Gen Diagram</i> node as shown in the screenshot below).</p>
<p><img src="screenshots/EditingDomainID.png"
alt="Keeping the Editing Domain IDs in sync" /></p>
<h2><a name="extending_the_editor_input">Extending the Editor
Input</a></h2>
<p>We have already modified the <tt>TopicmapEditor</tt> to create a
transactional <tt>EditingDomain</tt> instance. The next step is to pass
this instance to each page of the multi-page editor. An <tt>EditorPart</tt>
receives its input using some descendant of <tt>IEditorInput</tt>,
usually a <tt>FileEditorInput</tt>. In our use case, we need to include
a reference to our central <tt>EditingDomain</tt> instance. In general,
any implementation of <tt>IEditorInput</tt> could be used as input
object for the <tt>EditorPart</tt>s contained in the multi-page editor.
Fortunately, the GMF code base already contains a convenient wrapper for
our task: the <tt>FileEditorInputProxy</tt>.</p>
<p>While it is tempting to simply replace the editor's input with
the wrapped-up input, this should be avoided since it causes trouble e.
g. with the problem markers. As a rule of thumb, an editor should always
return the same input object that it was initialized with or that was
passed to it, no matter what happens internally. In our case, we just
add an attribute of type <tt>FileEditorInputProxy</tt> to the <tt>TopicmapEditor</tt>
and add a method for the lazy initialization of this attribute:</p>
<pre>
protected IEditorInput getWrappedInput() {
if (wrappedInput == null) {
if (getEditorInput() instanceof IFileEditorInput) {
wrappedInput = new FileEditorInputProxy((IFileEditorInput)getEditorInput(),
(TransactionalEditingDomain) getEditingDomain());
} else {
wrappedInput = getEditorInput();
}
}
return wrappedInput;
}</pre>
<p>You will note that references to <tt>FileEditorInputProxy</tt>
can not be resolved at this point - simply because this class is
provided by the GMF, and the EMF generated editor plug-in does not know
about the GMF yet. We now have two means to resolve this issue: either
add the dependencies to the EMF editor plug-in or "inherit" the
dependencies from the GMF editor plug-in. Let's take the second
approach: Open the <tt>org.example.emfgmf.editor</tt> plug-in manifest
and add a dependency to <tt>org.example.emfgmf.diagram</tt>. Then open
the <tt>org.example.emfgmf.diagram</tt> plug-in manifest and set the
flag <i>reexport this dependency</i> for all <tt>org.eclipse.gmf.*</tt>
plug-ins.</p>
<blockquote>
<p><img src="images/tip.gif" width="62" height="13" alt="Tip">
It's usually faster to change the file <tt>MANIFEST.MF</tt> manually.</p>
</blockquote>
<p>We also have to adapt the generated class <tt>TopicMapDocumentProvider</tt>.
You may have noted that this class refers to <tt>FileEditorInput</tt> in
many places, and this leads to trouble as soon as you pass it a <tt>FileEditorInputProxy</tt>.
Fortunately we can simply replace <tt>FileEditorInput</tt> with <tt>IFileEditorInput</tt>
in the entire class except for the instantiation in <tt>handleElementMoved()</tt>.
We then have to tell the <tt>TopicMapDocumentProvider</tt> not to create
its own Editing Domain, but to use the one provided by the <tt>FileEditorInputProxy</tt>.
The easiest way to do this is to adapt the method <tt>createEmptyDocument()</tt>:</p>
<pre>
protected IDocument createEmptyDocument() {
<b>return createEmptyDocument(null);</b>
}
<b>protected IDocument createEmptyDocument(Object input) {
DiagramDocument document = new DiagramDocument();
if (input instanceof FileEditorInputProxy) {
FileEditorInputProxy proxy = (FileEditorInputProxy) input;
document.setEditingDomain(proxy.getEditingDomain());
} else {
document.setEditingDomain(createEditingDomain());
}
return document;
}</b></pre>
<p>In addition to this, we have to change the call to <tt>createEmptyDocument()</tt>
in <tt>createDocument()</tt>:</p>
<pre>
protected IDocument createDocument(Object element) throws CoreException {
//...
<b>IDocument document = createEmptyDocument(element);</b>
setDocumentContent(document, (IEditorInput) element);
setupDocument(element, document);
return document;
}</pre>
<p>We won't be able to see the effects of these changes until later,
though.</p>
<h2><a name="dissecting_the_emf_editor">Dissecting the EMF
Editor</a></h2>
<p>The <tt>TopicmapEditor</tt> generated by the EMF is derived from
<tt>MultiPageEditorPart</tt>, and this class already provides means to
handle multiple editor pages. Unfortunately for us, the generated
example creates the editor pages using anonymous subclasses of <tt>ViewerPane</tt>.
This means that the <tt>TopicmapEditor</tt> itself has to override some
methods of the <tt>MultiPageEditorPart</tt> that are intended to save
the developer of descendants some work. While this is perfectly
acceptable for a generated example, it becomes a problem when adding the
GMF generated editor to the <tt>TopicmapEditor</tt>. Our next task will
therefore be to split the generated editor into separate <tt>EditorPart</tt>
subclasses. We will do so by creating subclasses of <tt>EditorPart</tt>
that will be responsible for one page each. Since we have six pages to
deal with, it's worth introducing a common superclass named <tt>TopicmapEditorPart</tt>
that extends <tt>EditorPart</tt> and implements <tt>IMenuListener</tt>.
We make this class abstract and add some common method implementations:</p>
<pre>
public abstract class TopicmapEditorPart extends EditorPart implements IMenuListener, IEditingDomainProvider {
protected TopicmapEditor parentEditor;
public TopicmapEditorPart(TopicmapEditor parent) {
super();
this.parentEditor = parent;
}
protected static String getString(String key) {
return TopicMapEditorPlugin.INSTANCE.getString(key);
}
public EditingDomain getEditingDomain() {
return parentEditor.getEditingDomain();
}
protected BasicCommandStack getCommandStack() {
return ((BasicCommandStack)getEditingDomain().getCommandStack());
}
protected AdapterFactory getAdapterFactory() {
return ((AdapterFactoryEditingDomain) ((FileEditorInputProxy)getEditorInput()).getEditingDomain()).getAdapterFactory();
}
protected void createContextMenuFor(StructuredViewer viewer) {
MenuManager contextMenu = new MenuManager("#PopUp");
contextMenu.add(new Separator("additions"));
contextMenu.setRemoveAllWhenShown(true);
contextMenu.addMenuListener(this);
Menu menu= contextMenu.createContextMenu(viewer.getControl());
viewer.getControl().setMenu(menu);
getSite().registerContextMenu(contextMenu, new UnwrappingSelectionProvider(viewer));
int dndOperations = DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_LINK;
Transfer[] transfers = new Transfer[] { LocalTransfer.getInstance() };
viewer.addDragSupport(dndOperations, transfers, new ViewerDragAdapter(viewer));
viewer.addDropSupport(dndOperations, transfers, new EditingDomainViewerDropAdapter(getEditingDomain(), viewer));
}
@Override
public void doSave(IProgressMonitor monitor) {
// nothing to do here - this is handled by the parent editor
}
@Override
public void doSaveAs() {
// nothing to do here - this is handled by the parent editor
}
@Override
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
setSite(site);
setInput(input);
}
@Override
public boolean isDirty() {
return getCommandStack().isSaveNeeded();
}
@Override
public boolean isSaveAsAllowed() {
return true;
}
public void menuAboutToShow(IMenuManager manager) {
// pass the request to show the context menu on to the parent editor
((IMenuListener)parentEditor.getEditorSite().getActionBarContributor()).menuAboutToShow(manager);
}
public abstract void setInput(Object input);
}</pre>
<p>Now that we've got the base class, we can create the <tt>EditorPart</tt>
derivates to represent the pages of the editor. Each of the editor pages
becomes a separate class, and since these classes look rather similar,
we'll only take a look at one of them as an example:</p>
<pre>
public class TableTreeEditorPart extends TopicmapEditorPart {
protected TreeViewer viewer;
public TableTreeEditorPart(TopicmapEditor parent) {
super(parent);
}
@Override
public void createPartControl(Composite parent) {
viewer = new TreeViewer(parent, SWT.NONE);
Tree tree = viewer.getTree();
tree.setLayoutData(new FillLayout());
tree.setHeaderVisible(true);
tree.setLinesVisible(true);
TreeColumn objectColumn = new TreeColumn(tree, SWT.NONE);
objectColumn.setText(getString("_UI_ObjectColumn_label"));
objectColumn.setResizable(true);
objectColumn.setWidth(250);
TreeColumn selfColumn = new TreeColumn(tree, SWT.NONE);
selfColumn.setText(getString("_UI_SelfColumn_label"));
selfColumn.setResizable(true);
selfColumn.setWidth(200);
viewer.setColumnProperties(new String [] {"a", "b"});
viewer.setContentProvider(new AdapterFactoryContentProvider(getAdapterFactory()));
viewer.setLabelProvider(new AdapterFactoryLabelProvider(getAdapterFactory()));
createContextMenuFor(viewer);
getEditorSite().setSelectionProvider(viewer);
}
@Override
public void setFocus() {
viewer.getTree().setFocus();
}
@Override
public void setInput(Object input) {
viewer.setInput(input);
}
}</pre>
<p>In order to get the <tt>ParentEditorPart</tt> working, we have to
extract the inner class <tt>ReverseAdapterFactoryContentProvider</tt>
from the generated <tt>TopicmapEditor</tt> class - fortunately, with the
refactoring capabilites of Eclipse, that's only a matter of a few mouse
clicks.</p>
<p>These classes are then instantiated by the <tt>TopicmapEditor</tt>
in its method <tt>createPages()</tt> - this is also where the code to
create the contents of each editor part originates from. The generated
attributes that hold the viewers are replaced by attributes that keep
the editor parts. The instantiation procedure now looks like this:</p>
<pre>
public void createPages() {
// Creates the model from the editor input
//
createModel();
// Only creates the other pages if there is something that can be edited
//
if (!getEditingDomain().getResourceSet().getResources().isEmpty() &amp;&amp;
!(getEditingDomain().getResourceSet().getResources().get(0)).getContents().isEmpty()) {
<b>try {
int pageIndex;
// Create a page for the selection tree view.
//
selectionEditorPart = new SelectionEditorPart(this);
pageIndex = addPage(selectionEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_SelectionPage_label"));
selectionEditorPart.setInput(getEditingDomain().getResourceSet());
// Create a page for the parent tree view.
//
parentEditorPart = new ParentEditorPart(this);
pageIndex = addPage(parentEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_ParentPage_label"));
// This is the page for the list viewer
//
listEditorPart = new ListEditorPart(this);
pageIndex = addPage(listEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_ListPage_label"));
// This is the page for the tree viewer
//
treeEditorPart = new TreeEditorPart(this);
pageIndex = addPage(treeEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_TreePage_label"));
// This is the page for the table viewer.
//
tableEditorPart = new TableEditorPart(this);
pageIndex = addPage(tableEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_TablePage_label"));
// This is the page for the table tree viewer.
//
tableTreeEditorPart = new TableTreeEditorPart(this);
pageIndex = addPage(tableTreeEditorPart, getWrappedInput());
setPageText(pageIndex, getString("_UI_TreeWithColumnsPage_label"));
} catch (PartInitException e) {
// add some error handling for production-quality coding
e.printStackTrace();
}</b>
getSite().getShell().getDisplay().asyncExec
(new Runnable() {
public void run() {
setActivePage(0);
}
});
}
// ...
}</pre>
<p>Having entrusted the handling of the editor parts to the <tt>MultipageEditorPart</tt>,
we can remove the attribute <tt>currentViewerPane</tt> and the methods <tt>setCurrentViewerPane()</tt>
and <tt>setFocus()</tt>. The reimplementation of <tt>isDirty()</tt> can
also be removed.</p>
<h2><a name="selection_handling">Selection Handling</a></h2>
<p>Now we have split up the generated editor, but this has disrupted
the generated coding for selection handling - we have to clean this up
before we move on. The <tt>MultiPageEditorPart</tt> already comes with a
mechanism to handle selection changes (see classes <tt>MultiPageEditorSite</tt>
and <tt>MultiPageSelectionProvider</tt>), but this mechanism is not used
by the generated coding - yet.</p>
<p>First we add a new attribute named <tt>selectionProvider</tt> of
type <tt>MultiPageSelectionProvider</tt> to the <tt>TopicmapEditor</tt>
and make the constructor of the editor class initialize this attribute
(passing itself as argument). Then we change the method <tt>init()</tt>
to set this instance as a site-wide selection provider instead of the
editor itself. The <tt>TopicmapEditor</tt> itself now no longer needs to
be a selection provider, so we can remove the interface implementation
of <tt>ISelectionProvider</tt> along with the generated attributes <tt>selectionChangedListener</tt>,
<tt>selectionChangedListeners</tt> and <tt>editorSelection</tt> as well
as the generated methods <tt>addSelectionChangedListener()</tt>, <tt>removeSelectionChangedListener()</tt>,
<tt>getSelection()</tt> and <tt>setSelection()</tt>. Note that <tt>setSelection()</tt>
contains a line to modify the status line manager - we'll move this call
into a separate listener that is initialized by the constructor:</p>
<pre>
public TopicmapEditor() {
super();
initializeEditingDomain();
<b>selectionProvider = new MultiPageSelectionProvider(this);
selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
setStatusLineManager(event.getSelection());
}
});</b>
}</pre>
<p>We still need to rewrite several references to the methods we
just removed. <tt>handleActivate()</tt> contains a call to <tt>setSelection()</tt>
that can easily be replaced by a call to the selection provider:</p>
<pre>
protected void handleActivate() {
// Recompute the read only state.
//
if (editingDomain.getResourceToReadOnlyMap() != null) {
editingDomain.getResourceToReadOnlyMap().clear();
// Refresh any actions that may become enabled or disabled.
//
<b>selectionProvider.setSelection(selectionProvider.getSelection());</b>
}
// ...
}</pre>
<p>We can now adapt the method <tt>setCurrentViewer()</tt> and throw
out everything that's concerned with selection handling:</p>
<pre>
public void setCurrentViewer(Viewer viewer) {
if (currentViewer != viewer) {
currentViewer = viewer;
}
}
</pre>
<p>Another method that requires a bit of attention is <tt>handleContentOutlineSelection()</tt>:</p>
<pre>
public void handleContentOutlineSelection(ISelection selection) {
if (!selection.isEmpty() &amp;&amp; selection instanceof IStructuredSelection) {
List selectedElements = ((IStructuredSelection)selection).toList();
if (getActiveEditor() == selectionEditorPart) {
// If the selection viewer is active, we want it to select the same selection as this selection.
//
selectionProvider.setSelection(new StructuredSelection(selectedElements));
} else {
// For any other viewer, set the input directly.
//
((TopicmapEditorPart)getActiveEditor()).setInput(selectedElements.get(0));
}
}
}
</pre>
<h2><a name="combining_the_editors">Combining the Editors</a></h2>
<p>Okay - now that we've completed the preparations (well, most of),
taping the editors together is surprisingly easy. We simply add a new
attribute named <tt>diagramEditor</tt> of type <tt>TopicMapDiagramEditor</tt>
to the <tt>TopicmapEditor</tt> and then extend the method <tt>createPages()</tt>
to include the diagram editor in the creation process:</p>
<pre>
public void createPages() {
// ...
try {
int pageIndex;
//...
// This is the page for the graphical diagram viewer.
//
diagramEditor = new TopicMapDiagramEditor();
pageIndex = addPage(diagramEditor, getEditorInput());
setPageText(pageIndex, "Diagram");
} catch (PartInitException e) {
// add some error handling for production-quality coding
e.printStackTrace();
}
// ...
}</pre>
<p>We also have to modify <tt>handleContentOutlineSelection()</tt>
again in order to keep the outline selection working:</p>
<pre>
public void handleContentOutlineSelection(ISelection selection) {
if (!selection.isEmpty() &amp;&amp; selection instanceof IStructuredSelection) {
List selectedElements = ((IStructuredSelection)selection).toList();
if (getActiveEditor() == selectionEditorPart) {
// If the selection viewer is active, we want it to select the same selection as this selection.
//
selectionProvider.setSelection(new StructuredSelection(selectedElements));
<b>} else if (getActiveEditor() == diagramEditor) {
// If the diagram viewer is active, we need to map the selection to the corresponding EditParts.
//
ArrayList&lt;Object&gt; selectionList = new ArrayList&lt;Object&gt;();
for(Object selectedElement: selectedElements) {
if (selectedElement instanceof EObject) {
String elementID = EMFCoreUtil.getProxyID((EObject) selectedElement);
selectionList.addAll(diagramEditor.getDiagramGraphicalViewer().findEditPartsForElement(elementID,
IGraphicalEditPart.class));
}
selectionProvider.setSelection(new StructuredSelection(selectionList));
}</b>
} else {
// For any other viewer, set the input directly.
//
((TopicmapEditorPart)getActiveEditor()).setInput(selectedElements.get(0));
}
}
}
</pre>
<p><i>Et voilà</i> - we've got the two editors combined in one
single multi-page editor.</p>
<p><img src="screenshots/IntegratedEMFEditor.png"
alt="Combined Editor with list-based page active" /></p>
<p><img src="screenshots/IntegratedGMFEditor.png"
alt="Combined Editor with graphical diagram page active" /></p>
<p>You may want to disable the standalone diagram editor at this
point - simply open the <tt>plugin.xml</tt> of the diagram editor and
remove the extension declaring the editor (extension point <tt>org.eclipse.ui.editors</tt>).
Note that the pre-packaged examples still contain this extension so that
you can compare the generated diagram editor and the integrated editors
more easily.</p>
<p>Although the integrated editor already looks quite impressive,
there are several important issues to fix.</p>
<ul>
<li>The properties view refuses to show any properties while the
graphical editor is active.</li>
<li>The menu items and toolbars of the graphical editor are still
missing.</li>
<li>Some of the actions in the Edit menu (like Cut, Copy, Paste
and Delete) don't work.</li>
<li>When creating a new file, the diagram is missing.</li>
<li>When expanding the topic map files in the resource navigator
view, a <tt>ClassCastException</tt> is thrown.</li>
</ul>
<h2><a name="taming_the_properties">Taming the Properties</a></h2>
<p>The reason for the properties view not behaving as desired is
simply that EMF uses the default properties mechanism while GMF already
uses the tabbed properties view. If you're not familiar with the tabbed
properties view, there's an <a
href="http://www.eclipse.org/articles/Article-Tabbed-Properties/tabbed_properties_view.html">excellent
article</a> available that also gives a hint to solve our current problem.
In order to use the tabbed properties for both the EMF and GMF generated
editor, we have to change the <tt>TopicmapEditor</tt> class so that it
implements <tt>ITabbedPropertySheetPageContributor</tt> and have the
method <tt>getContributorId()</tt> return <tt>diagramEditor.getContributorId()</tt>.
We also need to adapt <tt>getPropertySheetPage()</tt> to create a <tt>PropertiesBrowserPage</tt>:</p>
<pre>
public IPropertySheetPage getPropertySheetPage() {
if (propertySheetPage == null) {
<b>propertySheetPage = new PropertiesBrowserPage(this) {
public void setActionBars(IActionBars actionBars) {
super.setActionBars(actionBars);
getActionBarContributor().shareGlobalActions(this, actionBars);
}
};</b>
}
return propertySheetPage;
}</pre>
<p>Note that you also have to change the type of <tt>propertySheetPage</tt>.
If you try out the implementation so far, you will notice that the
diagram editor now works, but the EMF editors always show <i>Properties
not available</i> in the view. The reason for this is that we haven't yet
told the properties framework to handle our generated model classes. To
do so, open the diagram editor <tt>plugin.xml</tt> file, locate the
extension to <tt>org.eclipse.ui.views.properties.tabbed.propertySections</tt>
and extend the list of input object types:</p>
<pre>
&lt;extension point="org.eclipse.ui.views.properties.tabbed.propertySections"&gt;
&lt;propertySections contributorId="org.example.emfgmf.diagram"&gt;
&lt;!-- ... --&gt;
&lt;propertySection
id="property.section.domain"
tab="property.tab.domain"
class="org.example.emfgmf.topicmap.diagram.sheet.TopicMapPropertySection"&gt;
&lt;input type="org.eclipse.gmf.runtime.notation.View"/&gt;
&lt;input type="org.eclipse.gef.EditPart"/&gt;
<b>&lt;input type="org.example.emfgmf.topicmap.TopicMap"/&gt;
&lt;input type="org.example.emfgmf.topicmap.Topic"/&gt;
&lt;input type="org.example.emfgmf.topicmap.Association"/&gt;</b>
&lt;/propertySection&gt;
&lt;/propertySections&gt;
&lt;/extension&gt;</pre>
<p>Granted, this is not the nicest way to solve the problem, but it
does the trick: We now have tabbed properties throughout the editor.</p>
<p><img src="screenshots/TabbedProperties.png"
alt="Tabbed Properties View with EMF editor" /></p>
<h2><a name="completing_menus_and_toolbars">Completing Menus
and Toolbars</a></h2>
<p>In order to get the menu and toolbar contributions working, we
need two auxiliary classes. The first one is <a
href="files/TopicmapMultipageActionBarContributor.java"><tt>TopicmapMultipageActionBarContributor</tt></a>.
This is an implementation of the interface <tt>IEditorActionBarContributor</tt>
that will be used as an action bar contributor for the entire multi-page
editor. This implementation is basically a composite action bar
contributor that is responsible for enabling and disabling the actions
contributed by different aggregated subcontrollers when switching
between editor pages. This implementation is rather generic and could be
used for other multi-page editors with small modifications. In our case,
the <tt>TopicmapMultipageActionBarContributor</tt> will handle the
switching between two sets of actions contributed by the <tt>TopicmapActionBarContributor</tt>
and the <tt>TopicMapDiagramActionBarContributor</tt>.</p>
<p>The second class - <a href="files/SubActionBarsExt.java"><tt>SubActionBarsExt</tt></a>
- is a utility class for the <tt>TopicmapMultipageActionBarContributor</tt>.
It is used to collect the actions contributed by subordinate action bar
contributors.</p>
<p>We can simply place these files in the generated editor package;
they are just not inlined in this article because of their size. Next,
we need to adapt two of the existing methods to access the generated <tt>TopicmapActionBarContributor</tt>
properly using the newly created <tt>TopicmapMultipageActionBarContributor</tt>.
In <tt>TopicmapEditor</tt>, we change <tt>getActionBarContributor()</tt>
as shown below:</p>
<pre>
public EditingDomainActionBarContributor getActionBarContributor() {
return (TopicmapActionBarContributor) ((TopicmapMultipageActionBarContributor) getEditorSite()
.getActionBarContributor()).getTreeSubActionBars()
.getContributor();
}</pre>
<p>In <tt>TopicmapEditorPart</tt>, we have to change the
implementation of <tt>menuAboutToShow()</tt> like this:</p>
<pre>
public void menuAboutToShow(IMenuManager manager) {
// pass the request to show the context menu on to the parent editor
((TopicmapActionBarContributor) ((TopicmapMultipageActionBarContributor) parentEditor
.getEditorSite().getActionBarContributor())
.getTreeSubActionBars().getContributor())
.menuAboutToShow(manager);
}</pre>
<p>Finally, we have to register our new contributor in the plug-in
manifest.</p>
<p><img src="screenshots/MultipageABC.png"
alt="Register Multipage Action Bar Contributor" /></p>
<h2><a name="fixing_edit_menu_actions">Fixing Edit Menu Actions</a></h2>
<p>Another problem that arises when combining EMF and GMF generated
editors is that some of the actions inside the Edit menu are provided by
both editors in different ways. In order to get these actions working
the way they should, two slight adjustments need to be made: Let <tt>TopicmapEditor</tt>
implement the interface <tt>IDiagramWorkbenchPart</tt> and delegate the
method calls to <tt>diagramEditor</tt>. Next, open the diagram editor
plug-in manifest and change the extensions to <tt>org.eclipse.gmf.runtime.common.ui.services.action.globalActionHandlerProviders</tt>
to point to the topicmap editor:</p>
<p><img src="screenshots/GlobalActionHandler.png"
alt="Changing the Global Action Handler Provider Registration" /></p>
<p>A cleaner way to achieve the re-registration would be to copy the
entire extension to the editor plug-in manifest and set this extension
to provide its implementation with a higher priority.</p>
<h2><a name="creating_the_diagram">Creating the Diagram</a></h2>
<p>When creating a new topic map file, the combined editor will now
refuse to open the file because the resource contains no diagram. In
order to fix this, we need to change the method <tt>performFinish()</tt>
of the generated <tt>TopicmapModelWizard</tt>:</p>
<pre>
public boolean performFinish() {
try {
// Remember the file.
//
final IFile modelFile = getModelFile();
// Do the work within an operation.
//
WorkspaceModifyOperation operation =
new WorkspaceModifyOperation() {
@Override
protected void execute(IProgressMonitor progressMonitor) {
try {
// Create a resource set
//
ResourceSet resourceSet = new ResourceSetImpl();
// Get the URI of the model file.
//
URI fileURI = URI.createPlatformResourceURI(modelFile.getFullPath().toString(), true);
// Create a resource for this file.
//
Resource resource = resourceSet.createResource(fileURI);
// Add the initial model object to the contents.
//
EObject rootObject = createInitialModel();
if (rootObject != null) {
resource.getContents().add(rootObject);
}
<b>// Create the diagram
//
Diagram diagram = ViewService.createDiagram(rootObject,
TopicMapEditPart.MODEL_ID,
TopicMapDiagramEditorPlugin.DIAGRAM_PREFERENCES_HINT);
if (diagram != null) {
resource.getContents().add(diagram);
diagram.setName(fileURI.lastSegment());
diagram.setElement(rootObject);
}</b>
// Save the contents of the resource to the file system.
//
// ... and so on ...</pre>
<p>This will create the diagram right after the <tt>TopicMap</tt> is
created and added to the resource.</p>
<h2><a name="fixing_the_resource_navigator">Fixing the Resource
Navigator</a></h2>
<p>When you try to navigate into the contents of the file (that is,
try to expand it in the tree view of the resource navigator), you'll see
an unhandled exception with a stack trace similar to this:</p>
<pre>
java.lang.ClassCastException: org.example.emfgmf.topicmap.impl.TopicMapImpl cannot be cast to org.eclipse.gmf.runtime.notation.View
at org.example.emfgmf.topicmap.diagram.navigator.TopicMapNavigatorContentProvider.selectViewsByType(TopicMapNavigatorContentProvider.java:379)
at org.example.emfgmf.topicmap.diagram.navigator.TopicMapNavigatorContentProvider.getChildren(TopicMapNavigatorContentProvider.java:188)
at org.eclipse.ui.internal.navigator.extensions.SafeDelegateTreeContentProvider.getChildren(SafeDelegateTreeContentProvider.java:91)
at org.eclipse.ui.internal.navigator.extensions.SafeDelegateTreeContentProvider.getChildren(SafeDelegateTreeContentProvider.java:281)
at org.eclipse.ui.internal.navigator.extensions.SafeDelegateTreeContentProvider.getChildren(SafeDelegateTreeContentProvider.java:89)
at org.eclipse.ui.internal.navigator.NavigatorContentServiceContentProvider.internalGetChildren(NavigatorContentServiceContentProvider.java:251)
at org.eclipse.ui.internal.navigator.NavigatorContentServiceContentProvider.getChildren(NavigatorContentServiceContentProvider.java:643)
[...and so on...]</pre>
<p>This exception occurs because the diagram editor adds a content
provider that does not know how to handle the classes generated by the
EMF. The fix for this is simply to disable the corresponding provider in
the diagram editor's plug-in manifest:</p>
<p><img src="screenshots/DiagramContents.png"
alt="Disable Diagram Content Provider" /></p>
<h2><a name="rcp">Want some RCP with that?</a></h2>
<p>Until now, we've worked in a complete Eclipse workbench
environment. Both EMF and GMF support generating RCP editors as well. In
order to combine the generated editors in an RCP environment, a few
things need to be observed - it is not sufficient to just set the
properties in the EMF .genmodel and the GMF generator to RCP (although
that's the first step, of course).</p>
<p>In the following sections we'll discuss the differences from the
solution outlined above that apply to RCP applications. <b>You
should walk through the entire process for a non-RCP application at
least once before attempting to try this in the RCP environment</b> - after
all, these are just the <i>differences</i> in the process that won't
make much sense unless you know the process.</p>
<p>As with the workbench edition of the integrated editor, we've
provided both the <a href="files/rcp-projects-initial-state.zip">projects
in their unmodified state</a>, right after generation, as well as the <a
href="files/rcp-projects-final-state.zip">projects in their final
state</a>.</p>
<h2><a name="setting_the_stage">Setting the Stage</a></h2>
<p>Before we can try to integrate the generated editors, we first
need to setup some playground projects. This is covered by the
documentation provided for EMF and GMF, so let's not get lost in the
details. You can use the
<h3>One editor or <strike>another</strike> the same?</h3>
<p>One of the first things you'll notice when running the generated
editors is that you'll see two sets of "Open" and "Open URI" actions in
the file menu. However, an attempt to create an EMF-only file (using
File - New - Topicmap Model) will lead to a strange error message
stating that a "Diagram is not present in the resource". The reason for
this is that although the file is created using the EMF wizard (and thus
doesn't contain a diagram), the application tries to open the file with
the GMF generated editor. This happens because in both editor plug-ins,
the extensions to <tt>org.eclipse.ui.editors</tt> have the (file)
extension property set to "topicmap". Remove this value from the
GMF generated editor so that the RCP application will always use
the EMF generated editor. While you're at it, you can also remove the
extensions to <tt>org.eclipse.ui.actionSets</tt>,
<tt>org.eclipse.ui.bindings</tt> and <tt>org.eclipse.ui.commands</tt>
to clean up the file menu a little. <b>Make sure you've got the
EMF generated editor working before you attempt to perform any of
the actions outlined above.</b> You should then be able to follow
the steps outlined in section
<a href="#upgrading_the_editing_domain">Upgrading the Editing Domain</a>.</p>
<h3>Editor Input, revised</h3>
<p>In section <a href="#extending_the_editor_input">Extending
the Editor Input</a> we used a wrapper class to pass the <tt>FileEditorInput</tt>
instance along with the editing domain to all subordinate editors. When
building an RCP application, the programs have to operate on <tt>URIEditorInput</tt>
instances instead, so we can't use the same wrapper. Fortunately, it's
not that difficult to build a specialized wrapper:</p>
<pre>
public class URIEditorInputProxy extends URIEditorInput {
protected TransactionalEditingDomain editingDomain;
public URIEditorInputProxy(URI uri, TransactionalEditingDomain domain) {
super(uri);
this.editingDomain = domain;
}
public URIEditorInputProxy(IMemento memento, TransactionalEditingDomain domain) {
super(memento);
this.editingDomain = domain;
}
public URIEditorInputProxy(URI uri, String name, TransactionalEditingDomain domain) {
super(uri, name);
this.editingDomain = domain;
}
public TransactionalEditingDomain getEditingDomain() {
return editingDomain;
}
}</pre>
<p>Note that you should place this wrapper in the diagram editor
plug-in, for example in the package <tt>...diagram.part</tt>. Now we can
integrate this wrapper as outlined in section <a
href="#extending_the_editor_input">Extending the Editor Input</a>,
including the modifications to the document provider methods. Since the
document provider already supports <tt>URIEditorInput</tt> instances,
you only have to adapt the methods as outlined above. Note that you
still need to re-export the GMF dependencies from the diagram editor
plug-in because they are needed later on.</p>
<h3>Business as usual</h3>
<p>We can then continue as outlined in <a
href="#dissecting_the_emf_editor">Dissecting the EMF Editor</a>, <a
href="#selection_handling">Selection Handling</a> and <a
href="#combining_the_editors">Combining the Editors</a>, with some
minor adjustments due to the differences in handling the editor input.
After a brief moment of enthusiasm when we can see the integrated editor
for the first time, we continue through <a href="#taming_the_properties">Taming
the Properties</a>, <a href="#completing_menus_and_toolbars">Completing
Menus and Toolbars</a>, <a href="#fixing_edit_menu_actions">Fixing Edit
Menu Actions</a> and <a href="#creating_the_diagram">Creating the
Diagram</a>.</p>
<h2><a name="miscellaneous_notes">Miscellaneous Notes</a></h2>
<p>While the integrated editor in its current state is already
usable for many applications, some issues still remain. Since this is an
article to outline the general procedure and not a comprehensive book,
we can't really cover every aspect of the integration in detail. That
being said, here are a few notes to illustrate known issues, outline
some possible solutions and encourage further reading.</p>
<h3>File Extension Issues</h3>
<p>If you're encountering strange issues with the modification
listeners of your resource sets or other synchronization problems, check
that the file extension that is set in the diagram editor plug-in
manifest in the extension to <tt>org.eclipse.emf.ecore.extension.parser</tt>
matches the actual file extension set for the editor itself. This issue
usually occurs if you forget to set <i>Same File for Diagram And
Model</i> to <tt>true</tt> when <a href="#setting_the_stage">setting the
stage</a>.</p>
<h3>Adapting the Outline</h3>
<p>The current outline consists of a tree viewer that displays the
entire resource contents, including the diagram elements. This might not
exactly be what you need: Depending on your requirements, you might want
to hide the diagram elements, display the graphical outline or create a
different outline view altogether.</p>
<p>In order to hide the diagram elements, you could simply add a
filter to the tree viewer that's created in the <tt>TopicmapEditor</tt>
in <tt>getContentOutlinePage()</tt>:</p>
<pre>
// Set up the tree viewer.
//
contentOutlineViewer.setContentProvider(new AdapterFactoryContentProvider(adapterFactory));
contentOutlineViewer.setLabelProvider(new AdapterFactoryLabelProvider(adapterFactory));
<b>ViewerFilter[] outlineFilters = new ViewerFilter[1];
outlineFilters[0] = new ViewerFilter() {
@Override
public boolean select(Viewer viewer,
Object parentElement, Object element) {
return !(element instanceof View);
}
};
contentOutlineViewer.setFilters(outlineFilters);</b></pre>
<p>Displaying the graphical outline (or even some hybrid outline) is
a bit more difficult - you will want to take a look at the way the <tt>DiagramEditor</tt>
provides the outline using its inner class <tt>DiagramOutlinePage</tt>.</p>
<h3>Handling Multiple Editor Instances</h3>
<p>Eclipse allows the user to open the same file multiple times
using the same editor - it is even possible to "duplicate" an editor by
right-clicking its tab and choosing <i>New Editor</i> from the context
menu. Although this won't happen very often in real-life scenarios, you
should spend some time and make sure that the editor instances are
synchronized before selling the entire solution to your customers. This
also includes handling of the resource change notifications that cause
the editor to reload its contents if the file is changed by another
application (this is implemented inside the generated diagram editor and
should be relocated to the top-level multipage editor class).</p>
<h3>Unifying the Undo and Redo Actions</h3>
<p>The editor in its current state does not only provide a set of
undo and redo actions, it even provides two of them - one for the
tree-based editors and one for the diagram editors. This has the effect
that you can change something in the diagram, switch over to the tree
and change something else there, then switch back to the diagram and
undo - this will undo your first change in the diagram, not the second
one in the tree-based editor part. This could be changed by extending
the <tt>TopicmapMultipageActionBarContributor</tt> to replace the
individual undo and redo actions with a set of common actions for the
entire editor.</p>
<h3>Allow Multiple Diagram Files</h3>
<p>You might recall that we set <i>Same File for Diagram And
Model</i> to <tt>true</tt> when <a href="#setting_the_stage">setting the
stage</a>. The reason for this was that - as you now know - it is already a
non-trivial task to integrate the two generated editors if only one
diagram exists per data file. Of course you can also generate multiple
diagram editors and show them in multiple pages of your top-level editor
- but then you also have to implement some logic to keep track of your
diagram files, create or load them whenever necessary and make sure that
every embedded editor receives a complete resource set. While this is
entirely possible, it's outside the scope of this article.</p>
<h2><a name="conclusion">Conclusion</a></h2>
<p>As you may have noticed, it's not the integration of the editors
that accounts for most of the trouble - it's the preparation of the
editor generated by EMF and the cleaning up of lots of different issues
afterwards that takes up most of the time. Nevertheless, it is possible
to weld the editors together to form a single, consistent user
interface. It would be great if the EMF and GMF teams could join their
efforts to make this task less painful in future versions.</p>
<h2><a name="acknowledgements">Acknowledgements</a></h2>
<p>This document comprises tips and tricks issued by many people in
many different locations. Thanks to <a
href="http://hse-at-work.blogspot.com">Seweryn Niemiec</a> who created
a first <a href="http://docs.google.com/View?docid=dcqc65c5_168kvf4">short
documentation</a> on how to get the editors synchronized, as well as to all
the readers and beta-testers of the contents of this article. Many kudos
to all the people behind EMF, GMF and Eclipse who made this possible in
the first place and never (OK, seldomly) seem to be bothered by
more-or-less-stupid questions appearing in their newsgroups.</p>
</div>
</body>
</html>