Skip to content

Tag: properties

Creating an OPM GEF Editor – Part 16: Displaying EMF Properties in a GEF Editor

Previous Tutorial: Creating an OPM GEF Editor – Part 15: Adding Structural Links.

The eclipse workbench provides us with ready-made views, which can be populated by the editor to support the information that is provided by it. One very useful views is the properties view, which can show us name-value properties of the diagram elements.
There are some properties tutorials in the net, like this, this and this; But none of these seemed to match my requirements. I also found that in the past there were problems mixing EMF and GEF (see here) but there was no elegant solution to the problem that didn’t require serious rewriting of the classes provided by either of the frameworks. So against my will I had to do some custom-made wrapper classes for things to work. But I’m getting ahead of myself…

When implementing a GEF editor, there are (at least) two ways to display properties in the properties view, both of them by use of the adapter pattern that is strongly used in eclipse (both the GEF GraphicalEditor and GraphicalEditPart implement the IAdaptable interface).

  1. Providing a IPropertySheetPage when the editor is requested to adapt to this type.
  2. Providing a IPropertySource when an EditPart is requested to adapt to this type.

Implementing the second one is fairly straightforward but probably requires a lot of code (or changes in the hierarchy and stuff). The first options is more generic and suited me more, and it was also partly solved in the net (undo didn’t work), so I decided to complete the implementation here. BTW, the second option is implemented in the ecore editor example that can be found in the eclipse GEF repository.

The GEF framework provides a IPropertySheetPage implementation called UndoablePropertySheetPage, which is called such because in its implementation it automatically creates SetPropertyValueCommand instances to change the properties, and these commands can be used to undo changes. A IPropertySheetPage requires a IPropertySourceProvider which is used to get the properties of an object (which come as IPropertySource instances) to display them in a property view.
First problem I encountered is that for some (sad) reason the EMF framework works very similar interfaces to the standard eclipse interfaces, using what they call a IItemPropertySource, so this is the first place where we start building bridges. Thankfully, EMF provides a class called AdapterFactoryContentProvider which receives the IItemPropertySource factory genereated by EMF and transforms the output into regular eclipse IPropertySource instances.
The second obstacle in the implementation was caused by the fact that when a diagram element is selected, the IPropertySourceProvider receives the EditPart as a parameter but we want to fetch the properties from the model that is inside the EditPart. Therefore we had to implement our own IPropertySourceProvider which retrieved the model from the EditPart before adapting it to a IPropertySource
Third and last hurdle was fixing the undo problem. This problem shows up as a ClassCastException like this:

java.lang.ClassCastException: org.eclipse.emf.edit.provider.ItemPropertyDescriptor$PropertyValueWrapper cannot be cast to java.lang.String
	at com.vainolo.phd.opm.model.impl.OPMThingImpl.eSet(OPMThingImpl.java:131)
	at org.eclipse.emf.ecore.impl.BasicEObjectImpl.eSet(BasicEObjectImpl.java:1081)
	at org.eclipse.emf.edit.provider.ItemPropertyDescriptor.setPropertyValue(ItemPropertyDescriptor.java:1437)
	at org.eclipse.emf.edit.ui.provider.PropertySource.setPropertyValue(PropertySource.java:116)
	at com.vainolo.phd.opm.gef.editor.OPMGraphicalEditor$UnwrappingPropertySource.setPropertyValue(OPMGraphicalEditor.java:207)
	at org.eclipse.gef.ui.properties.SetPropertyValueCommand.undo(SetPropertyValueCommand.java:195)
	at org.eclipse.gef.commands.CompoundCommand.undo(CompoundCommand.java:173)
	at org.eclipse.gef.commands.ForwardUndoCompoundCommand.undo(ForwardUndoCompoundCommand.java:52)
	at org.eclipse.gef.commands.CommandStack.undo(CommandStack.java:395)
	at org.eclipse.gef.ui.actions.UndoAction.run(UndoAction.java:94)
	at org.eclipse.jface.action.Action.runWithEvent(Action.java:498)
	at org.eclipse.ui.actions.RetargetAction.runWithEvent(RetargetAction.java:230)
	at org.eclipse.jface.commands.ActionHandler.execute(ActionHandler.java:119)

Some code debugging showed that EMF stores the property values in something called a PropertyValueWrapper. When the SetPropertyValueCommand is executed, it stores the property’s old value, and when undo is called, it tries to apply it, but because it is a wrapper and not a real value, an exception is thrown. Reading the SetPropertyValueCommand.execute code, it seems that this is not the only time things are wrapped, because when the command stores the old value of the property, it calls the unwrapValue method… but this only unwraps instances of IPropertySource. It took me the better part of half a day to think of an elegant solution to this problem which didn’t require changes either to the SetPropertyValueCommand or the UndoablePropertySheetPage or other framework classes, and at the end I decided to implement a new IPropertySource which automatically unwraps a property’s value (I called this class UnwrappingPropertySource. Good name, right?). After doing this and plugging all things together, voila!

Since I am not an eclipse expert, I may have missed something in my implementation, but it seems to work, so here is the full code of the OPMGraphicalEditor (properties code at the end).

package com.vainolo.phd.opm.gef.editor;

import java.io.IOException;
import java.util.EventObject;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.edit.provider.ItemPropertyDescriptor.PropertyValueWrapper;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.gef.DefaultEditDomain;
import org.eclipse.gef.EditDomain;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.commands.CommandStack;
import org.eclipse.gef.palette.PaletteRoot;
import org.eclipse.gef.ui.parts.GraphicalEditorWithFlyoutPalette;
import org.eclipse.gef.ui.properties.UndoablePropertySheetEntry;
import org.eclipse.gef.ui.properties.UndoablePropertySheetPage;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.properties.IPropertySourceProvider;
import org.eclipse.ui.views.properties.PropertySheetPage;

import com.vainolo.phd.opm.gef.editor.part.OPMEditPartFactory;
import com.vainolo.phd.opm.model.OPMPackage;
import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
import com.vainolo.phd.opm.model.provider.OPMItemProviderAdapterFactory;

public class OPMGraphicalEditor extends GraphicalEditorWithFlyoutPalette {

    private Resource opdResource;
    private OPMObjectProcessDiagram opd;

    PropertySheetPage propertyPage;

    /**
     * Initialize the {@link EditDomain} of the editor.
     */
    public OPMGraphicalEditor() {
        setEditDomain(new DefaultEditDomain(this));
    }

    @Override
    protected void initializeGraphicalViewer() {
        super.initializeGraphicalViewer();
        getGraphicalViewer().setContents(opd);
    }

    @Override
    protected void configureGraphicalViewer() {
        super.configureGraphicalViewer();
        getGraphicalViewer().setEditPartFactory(new OPMEditPartFactory());
    }

    @Override
    protected PaletteRoot getPaletteRoot() {
        return new OPMGraphicalEditorPalette();
    }

    /**
     * Save the model using the resource from which it was opened, and mark the
     * current location in the {@link CommandStack}.
     */
    @Override
    public void doSave(IProgressMonitor monitor) {
        if(opdResource == null) {
            return;
        }

        try {
            opdResource.save(null);
            getCommandStack().markSaveLocation();
        } catch (IOException e) {
            // TODO do something smarter.
            e.printStackTrace();
            opdResource = null;
        }
    }

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

        loadInput(input);
    }

    private void loadInput(IEditorInput input) {
        OPMPackage.eINSTANCE.eClass(); // This initializes the OPMPackage
                                       // singleton implementation.
        ResourceSet resourceSet = new ResourceSetImpl();
        if(input instanceof IFileEditorInput) {
            IFileEditorInput fileInput = (IFileEditorInput) input;
            IFile file = fileInput.getFile();
            opdResource = resourceSet.createResource(URI.createURI(file.getLocationURI().toString()));
            try {
                opdResource.load(null);
                opd = (OPMObjectProcessDiagram) opdResource.getContents().get(0);
            } catch (IOException e) {
                // TODO do something smarter.
                e.printStackTrace();
                opdResource = null;
            }
        }
    }

    /**
     * Fire a {@link IEditorPart#PROP_DIRTY} property change and call super
     * implementation.
     */
    @Override
    public void commandStackChanged(EventObject event) {
        firePropertyChange(PROP_DIRTY);
        super.commandStackChanged(event);
    }

    /**
     * This methos implements adapting to {@link IPropertySheetPage}. All other requests are
     * forwarded to the {@link GraphicalEditorWithFlyoutPalette#getAdapter(Class) parent}
     * implementation.
     */
    @Override
    public Object getAdapter(@SuppressWarnings("rawtypes") Class type) {
        if(type.equals(IPropertySheetPage.class)) {
            if(propertyPage == null) {
                propertyPage = (UndoablePropertySheetPage) super
                        .getAdapter(type);
                // A new PropertySourceProvider was implemented to fetch the model
                // from the edit part when required. The property source is provided
                // by the generated EMF classes and wrapped by the AdapterFactoryContentProvider
                // to yield standard eclipse interfaces.
                IPropertySourceProvider sourceProvider = new IPropertySourceProvider() {
                    IPropertySourceProvider modelPropertySourceProvider = new AdapterFactoryContentProvider(new OPMItemProviderAdapterFactory());

                    @Override
                    public IPropertySource getPropertySource(Object object) {
                        IPropertySource source = null;
                        if(object instanceof EditPart) {
                            source = modelPropertySourceProvider.getPropertySource(((EditPart) object).getModel());
                        } else {
                            source = modelPropertySourceProvider.getPropertySource(object);
                        }

                        if(source != null) {
                            return new UnwrappingPropertySource(source);
                        } else {
                            return null;
                        }
                    }

                };
                UndoablePropertySheetEntry root = new UndoablePropertySheetEntry(getCommandStack());
                root.setPropertySourceProvider(sourceProvider);
                propertyPage.setRootEntry(root);
            }
            return propertyPage;
        }
        return super.getAdapter(type);
    }

    /**
     * A property source which unwraps values that are wrapped in an EMF
     * {@link PropertyValueWrapper}
     *
     * @author vainolo
     *
     */
    public class UnwrappingPropertySource implements IPropertySource {
        private IPropertySource source;

        public UnwrappingPropertySource(final IPropertySource source) {
            this.source = source;
        }

        @Override
        public Object getEditableValue() {
            Object value = source.getEditableValue();
            if(value instanceof PropertyValueWrapper) {
                PropertyValueWrapper wrapper = (PropertyValueWrapper) value;
                return wrapper.getEditableValue(null);
            } else {
                return source.getEditableValue();
            }
        }

        @Override
        public IPropertyDescriptor[] getPropertyDescriptors() {
            return source.getPropertyDescriptors();
        }

        @Override
        public Object getPropertyValue(Object id) {
            Object value = source.getPropertyValue(id);
            if(value instanceof PropertyValueWrapper) {
                PropertyValueWrapper wrapper = (PropertyValueWrapper) value;
                return wrapper.getEditableValue(null);
            } else {
                return source.getPropertyValue(id);
            }
        }

        @Override
        public boolean isPropertySet(Object id) {
            return source.isPropertySet(id);
        }

        @Override
        public void resetPropertyValue(Object id) {
            source.resetPropertyValue(id);
        }

        @Override
        public void setPropertyValue(Object id, Object value) {
            source.setPropertyValue(id, value);
        }
    }
}

As usual, I really hope that this tutorial has helped you in some way. I enjoy writing them and learning while preparing them.

Next Tutorial: Creating an OPM GEF Editor – Part 17: How to Define Container Edit Parts.