We are used to clicking things in Eclipse and the properties view getting updated to show us additional information about the thing we clicked. However, when clicking on elements in the Eclipse GLSP editor this is not the case. Next, I give some background on how the PropertiesView works and how it is related to the editor selection. Then, I show how we can modify the GLSP editor to make use of the properties view.
Current State
The GLSP tooling, I have found, is really complete out of the box. My issue is that most of the extra tools and things that are already available for you is poorly documented, so you either have to reinvent the wheel, or take some time to go over the code base and find what you need. This was the case with the properties view. In this discussion, one of the GLSP maintainers suggests to create a SelectAction handler and then use that to notify the Eclipse Selection Service. But taking a look at the code, the GLSP IDE editor plugin provides an IdeSelectActionHandler and IdeSelectAllActionHandler that already does that, and that notifies the GLSP Editor about the selection changes. All you need to do, is bind them in your editor module:
@Override
protected void configureActionHandlers(final MultiBinding<ActionHandler> bindings) {
super.configureActionHandlers(bindings);
...
bindings.add(IdeSelectActionHandler.class);
bindings.add(IdeSelectAllActionHandler.class);
}
To be precise, the notification is received by the GLSPDiagramComposite (which is used by the GLSPDiagramEditor to provide the editor part control – an embedded browser if you need to know). The magic happens in the updateSelection method:
public void updateSelection(final SelectAction selectAction) {
getModelStateOnceInitialized().thenAccept(modelState -> {
Collection<String> selectedIds = selectAction.getSelectedElementsIDs();
Collection<String> deselectedIds = selectAction.isDeselectAll()
? modelState.getIndex().allIds()
: selectAction.getDeselectedElementsIDs();
List<GModelElement> selectedGModelElements = toGModelElements(selectedIds, modelState);
List<GModelElement> deselectedGModelElements = toGModelElements(deselectedIds, modelState);
List<GModelElement> selection = toGModelElementStream(currentSelection).collect(toList());
selection.removeAll(deselectedGModelElements);
addUnique(selectedGModelElements, selection);
currentSelection = new StructuredSelection(selection);
selectionListener.selectionChanged(new SelectionChangedEvent(this, currentSelection));
});
}
In the method, the selected element IDs (selectAction.getSelectedElementsIDs()) are mapped to Graph Model Elements via the index. Then, the selectionListener is notified with the updated list of Graph Model Elements that are currently selected. In this case, the selection listener is just a wrapper to make sure that the notifications are run in the UI thread. Since the GLSPDiagramComposite was used as the selection provider for the GLSPDiagramEditor:
@Override
public void createPartControl(final Composite parent) {
diagram.createPartControl(parent);
setPartName(generatePartName());
getSite().setSelectionProvider(diagram);
createBrowserMenu();
}
the PropertiesView will be notified that a Graph Model element was selected. Yet… nothing shows in the properties view. To understand why, we need to look at how the PropertiesView work.
Eclipse Properties View
There is an Eclipse Corner Article that explains how to use the properties view. In a nutshell, there are two ways to provide a property source for a particular selection:
– The selected item can implement the IAdaptable interface and return an IPropertySource object when getAdapter is called.
– The selected item can implement the IPropertySource interface and provide the properties.
Neither of which GModelElement satisfies (the super type of selected elements). So in order to solve the issue, we would need to make sure that the updateSelection method that we discussed previously wraps the Graph Model Elements in a custom IAdaptable or IPropertySource implementation. Before you go and starting doing this, take a moment to understand the extend of the work needed to provide a working IPropertySource.
IPropertySource
The IPropertySource interface allows the Properties View to dynamically adjust itself to show the properties of any Object within eclipse. It does so, in its simplest way, by providing a set of IPropertyDescriptor. Each property descriptor will be visually represented by a row in the properties view (but technically provided by the PropertySheetEntry). Without dwelling into the details of the API, the critical bit is that IPropertyDescriptor must provide the editor for changing the property value. This can be a simple text editor for changing a text attribute or something like a color picker if the attribute represents a color. But most importantly you must react to the editor changes so you can update the object properties accordingly.
I must confess I panicked a bit when I understood what had to be done. When I was about to tell my boss I needed another month to get this working, I remembered that EMF provides this functionality out of the box. Luckily for me, the domain model (i.e. the mode for which the graph is created) was an EMF model, so all I had to do was hook everything together. If your domain model is not an EMF model…. this is as far as I can help. You need to implement all the property descriptors… (or create an Ecore model of your domain and use the solution that I present next – which I would strongly suggest and offer to help).
The Emf Edit and Editor
If you haven’t done so, its a good time to open your Genmodel and generate your edit and editor plugins. If you have no idea about what I just said, then take some time to read about EMF code generation capabilities. Vogella has a nice tutorial.
There are two main aspects that will help us tie all together. First, in your ‘edit’ plugin, EMF must have generated an XxxItemProviderAdapterFactory. This class can be used to adapt your domain models and create the required property descriptors. As a result, EMF will provide the required editors for your object features and handle the model updating. Required implementation: 0! Well, unless you want to provide a custom editor… in which case I will recommend sticking to the generated code as much as possible so all the complicated bits are handled for you. Your properties view will behave the same as when using the generated editor.
Second, understand how the editor uses the XxxItemProviderAdapterFactory to connect it all together. EMF does this by providing a custom IPropertySheetPage (a.k.a the properties view). This happens in two places in the XxxEditor (in the editor plugin):
@Override
public <T> T getAdapter(Class<T> key) {
if (key.equals(IContentOutlinePage.class)) {
return showOutlineView() ? key.cast(getContentOutlinePage()) : null;
} else if (key.equals(IPropertySheetPage.class)) {
return key.cast(getPropertySheetPage());
} else if (key.equals(IGotoMarker.class)) {
return key.cast(this);
} else {
return super.getAdapter(key);
}
}
that indicates to Eclipse that we want to provide custom outline, properties and goto marker implementations. And:
public IPropertySheetPage getPropertySheetPage() {
PropertySheetPage propertySheetPage = new ExtendedPropertySheetPage(editingDomain,
ExtendedPropertySheetPage.Decoration.NONE, null, 0, false) {
@Override
public void setSelectionToViewer(List<?> selection) {
WfpEditor.this.setSelectionToViewer(selection);
WfpEditor.this.setFocus();
}
@Override
public void setActionBars(IActionBars actionBars) {
super.setActionBars(actionBars);
getActionBarContributor().shareGlobalActions(this, actionBars);
}
};
propertySheetPage.setPropertySourceProvider(new AdapterFactoryContentProvider(adapterFactory));
propertySheetPages.add(propertySheetPage);
return propertySheetPage;
}
Of importance is line 14, in which we attach the XxxItemProviderAdapterFactory (adapterFactory) to the properties view. This part is the key, because in the IPropertySheetEntry implementation, the propertySourceProvider of the view is used to get the selected object’s IPropertySource.
protected IPropertySource getPropertySource(Object object) {
if (sources.containsKey(object))
return sources.get(object);
IPropertySource result = null;
IPropertySourceProvider provider = propertySourceProvider;
if (provider == null && object != null) {
provider = Adapters.adapt(object, IPropertySourceProvider.class);
}
if (provider != null) {
result = provider.getPropertySource(object);
} else {
result = Adapters.adapt(object, IPropertySource.class);
}
sources.put(object, result);
return result;
}
Hocking it up
So with the required information in place, we need to modify our GLSP editor to use our generated XxxItemProviderAdapterFactory. Our first modification would be to the composite’s update selection method. For this we need to extend the GLSPDiagramComposite:
public class WfpGLSPDiagramComposite extends GLSPDiagramComposite {
public WfpGLSPDiagramComposite(
final String editorId,
PropertySheetPage propertySheetPage) {
super(editorId);
this.propertySheetPage = propertySheetPage;
}
/**
* Updates the currently selected elements.
* <p>
* This implementation mapes the selected Graph Model Elements to their semantic counterparts,
* and notifies the selection listeners with a selection of semantic elements.
* </p>
*
* @param selectAction the {@link SelectAction}
*/
@Override
public void updateSelection(final SelectAction selectAction) {
getModelStateOnceInitialized().thenAccept(modelState -> {
Set<String> selectedIds = new HashSet<>(selectAction.getSelectedElementsIDs());
Set<String> deselectedIds = new HashSet<>(selectAction.isDeselectAll() ?
modelState.getIndex().allIds()
: selectAction.getDeselectedElementsIDs());
Set<String> selection = this.currentSelection.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.collect(Collectors.toCollection(() -> new HashSet<>()));
selection.removeAll(deselectedIds);
selection.addAll(selectedIds);
currentSelection = new StructuredSelection(new ArrayList<>(selection));
if (modelState instanceof SemanticModelState) {
this.getInstance(AdapterFactory.class)
.thenAccept(af -> {
if (af instanceof IChangeNotifier) {
// Remove and add so we don't get multiple registrations
((IChangeNotifier) af).removeListener(gslpUpdateProvider);
((IChangeNotifier) af).addListener(gslpUpdateProvider);
}
var provider = new AdapterFactoryContentProvider(af);
Display.getDefault().syncExec(new Runnable() {
public void run() {
propertySheetPage.setPropertySourceProvider(provider);
}
});
var semSelection = selection.stream()
.map(id -> ((SemanticModelState) modelState).getIndex().get(id))
.filter(Optional::isPresent)
.map(Optional::get)
.map(ge -> ((SemanticModelState) modelState).getIndex().semanticElement(ge))
.filter(Optional::isPresent)
.map(Optional::get)
.distinct()
.toList();
this.selectionListener.selectionChanged(new SelectionChangedEvent(this, new StructuredSelection(semSelection)));
});
} else {
selectionListener.selectionChanged(new SelectionChangedEvent(this, currentSelection));
}
});
}
private PropertySheetPage propertySheetPage;
private GslpUpdateProvider gslpUpdateProvider = new GslpUpdateProvider();
private class GslpUpdateProvider implements INotifyChangedListener {
@Override
public void notifyChanged(Notification notification) {
getInstance(ModelSubmissionHandler.class)
.thenAccept(handler -> {
handler.submitModel("PropertiesView change").forEach(a -> dispatch(a));
});
}
}
}
The first modification is that we accept a PropertySheetPage in the constructor. This is to emulate the EMF editor implementation, without providing a custom property sheet page. The reference to the PropertySheetPage enables us to set our property source provider (line 44). The other deviation from the original implementation is that we keep the current selection as a list of element IDs (this is in order to avoid duplicate Graph Model elements in the selection). In lines 47-55 we use the selected element IDs to get the GraphElements from the index, and from their resolve their respective semantic (domain) elements. Since these will be instances of our Ecore metamodel, the AdapterFactoryContentProvider will resolve their IPropertySources. Finally, notice that the AdapterFactory is injected. This allows this custom Diagram composite to be used with any Ecore metamodel. The adapter factory is injected in the editor module:
public class EclipseDiagramModule extends DiagramModule {
@Override
public void configure() {
super.configure();
bind(ModelInitializationConstraint.class).to(DefaultModelInitializationConstraint.class).in(Scopes.SINGLETON);
bind(AdapterFactory.class).to(XxxItemProviderAdapterFactory.class).in(Scopes.SINGLETON);
}
...
The other piece of additional code is the GslpUpdateProvider class. This is a simple EMF notification listener that we will attach the AdapterFactory in order to be informed when the domain elements are nodified via the PropertiesView. In this case, we use the ModelSubmissionHandler to notify the client that the model has been modified and the graph should be redrawn. Further filtering could be done to determine if the change merits a graph redraw (e.g. a non-visual) property change can be skipped.
Next, we extend the GLSPDiagramEditor to provide our custom composite. It also makes sure we control the PropertySheetPage used for the editor.
public class XxxGLSPDiagramEditor extends GLSPDiagramEditor {
@Override
public <T> T getAdapter(Class<T> key) {
if (key.equals(IPropertySheetPage.class)) {
return key.cast(this.propertySheetPage);
}
return super.getAdapter(key);
}
@Override
protected GLSPDiagramComposite createGLSPDiagramComposite() {
return new WfpGLSPDiagramComposite(getEditorId(), this.propertySheetPage);
}
private PropertySheetPage propertySheetPage = new PropertySheetPage();
}
Finally, we need to update our org.eclipse.ui.editors extension to use the new Editor:
<extension
point="org.eclipse.ui.editors">
<editor
class="my.project.glsp.ide.editor.ui.XxxGLSPDiagramEditor"
contributorClass="org.eclipse.glsp.ide.editor.ui.GLSPDiagramEditorActionContributor"
...
And that’s it. Now, when you click on an element in your diagram, your properties editor should show you all the properties, let you modify them and refresh the graph when changes occur. If not working, the best place to start is the PropertySheetEntry class, in the getPropertySource method. There, you can check that thar the property source provider is set, and how it is working.