blob: 91f0f6fed1d2e6909d4ec95fcb44841a2e653461 [file] [log] [blame]
/*********************************************************************************
* Copyright (c) 2020-2021 Robert Bosch GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Robert Bosch GmbH - initial API and implementation
********************************************************************************
*/
package org.eclipse.app4mc.visualization.ui;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang.ClassUtils;
import org.eclipse.app4mc.visualization.ui.registry.ModelVisualization;
import org.eclipse.app4mc.visualization.ui.registry.ModelVisualizationRegistry;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.di.extensions.Service;
import org.eclipse.e4.ui.di.PersistState;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.menu.MDirectToolItem;
import org.eclipse.e4.ui.model.application.ui.menu.MToolBarElement;
import org.eclipse.e4.ui.services.IServiceConstants;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
@SuppressWarnings("restriction")
public class VisualizationPart {
/**
* The ID that is used in the model fragment for this part.
*/
public static final String ID = "org.eclipse.app4mc.visualization.ui.partdescriptor.app4mcvisualizations";
private static final String LAST_SELECTED_STATE = "LAST_SELECTED";
@Inject
@Service
ModelVisualizationRegistry registry;
/**
* The {@link IEclipseContext} of this part.
*/
@Inject
IEclipseContext partContext;
/**
* The parent {@link Composite} of this part.
*/
Composite parentComposite;
/**
* The {@link Composite} on which the visualization is rendered.
*/
Composite visualizationComposite;
/**
* The model type that should be visualized.
*/
List<Class<?>> activeTypes;
/**
* The model elements that are used for the visualization.
*/
List<?> activeModelElements;
/**
* The current active rendered visualization.
*/
ModelVisualization activeVisualization;
/**
* The list of all available visualizations available for the current {@link #activeType}.
*/
List<ModelVisualization> availableModelVisualizations;
/**
* The {@link IEclipseContext} that is created for the visualization rendering.
*/
IEclipseContext activeContext;
/**
* <code>true</code> if the selection listener is disabled, <code>false</code>
* if the visualization is updated on selection changes.
*/
boolean pinned = false;
/**
* Mapping of model type to visualization id to remember the last selected visualization.
*/
HashMap<String, String> lastSelected = new HashMap<>();
private boolean adapterEnabled = true;
/**
* EMF Adapter to reload the visualization on model property changes.
*/
Adapter updateViewAdapter = new AdapterImpl() {
@Override
public void notifyChanged(Notification msg) {
if (adapterEnabled && activeVisualization != null) {
showVisualization(activeVisualization.getId());
}
}
};
@PostConstruct
public void postConstruct(Composite parent, MPart part,
@Optional @Named(IServiceConstants.ACTIVE_SELECTION) ISelection selection) {
parentComposite = parent;
parentComposite.setLayout(new FillLayout());
parentComposite.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
if (visualizationComposite == null) {
visualizationComposite = new Composite(parent, SWT.NONE);
visualizationComposite.setLayout(new FillLayout());
if (selection != null) {
handleSelection(selection);
} else {
showEmpty(visualizationComposite);
}
}
// load previous selected states
Map<String, String> state = part.getPersistedState();
String lastSelectedString = state.get(LAST_SELECTED_STATE);
if (lastSelectedString != null) {
lastSelectedString = lastSelectedString.substring(1, lastSelectedString.length() - 1);
String[] keyValues = lastSelectedString.split(",");
for (String entry : keyValues) {
String[] keyValue = entry.split("=");
if (keyValue.length == 2) {
lastSelected.put(keyValue[0], keyValue[1]);
}
}
}
// ensure the checked state of the pin tool item is reset
for (MToolBarElement element : part.getToolbar().getChildren()) {
if (element.getElementId().equals("org.eclipse.app4mc.visualization.ui.directtoolitem.pin")) {
MDirectToolItem toolItem = (MDirectToolItem) element;
toolItem.setSelected(pinned);
}
}
}
/**
* Disposes a current visualization and renders the visualization for the
* current active model element.
*
* @param visualizationId The ID of the visualization to show. Can be
* <code>null</code> which results in showing the first
* available visualization.
*/
public void showVisualization(String visualizationId) {
showVisualization(visualizationId, false);
}
/**
* Disposes a current visualization and renders the visualization for the
* current active model element.
*
* @param visualizationId The ID of the visualization to show. Can be
* <code>null</code> which results in showing the first
* available visualization.
* @param reload <code>true</code> if this method is called to reload a
* visualization, <code>false</code> if a new
* visualization should be opened.
*/
public void showVisualization(String visualizationId, boolean reload) {
if (parentComposite == null || (!reload && isPinned())) {
return;
}
// clear any current active visualization
if (visualizationComposite != null) {
if (activeContext != null && activeVisualization != null) {
ContextInjectionFactory.invoke(activeVisualization, PreDestroy.class, activeContext, null);
activeContext.dispose();
}
visualizationComposite.dispose();
}
// create a new Composite as parent for the visualization
visualizationComposite = new Composite(parentComposite, SWT.NONE);
visualizationComposite.setLayout(new FillLayout());
// find the visualization for the current active model type and the given id
availableModelVisualizations = registry.getVisualizations(activeTypes);
if (!availableModelVisualizations.isEmpty()) {
if (visualizationId != null) {
activeVisualization = availableModelVisualizations.stream()
.filter(mv -> mv.getId().equals(visualizationId))
.findFirst()
.orElse(null);
// only handle if there is a visualization for the given id
if (activeVisualization != null) {
lastSelected.put(activeTypes.get(0).getName(), visualizationId); // ???
} else {
// take the first returned available visualization
activeVisualization = availableModelVisualizations.get(0);
}
} else {
// take the first returned available visualization
activeVisualization = availableModelVisualizations.get(0);
}
if (activeVisualization != null) {
activeContext = partContext.createChild(activeTypes.get(0) + " Visualization"); // ???
activeContext.set(Composite.class, visualizationComposite);
activeContext.set(activeVisualization.getType(), activeModelElements.get(0));
activeContext.set(List.class, activeModelElements);
ContextInjectionFactory.invoke(activeVisualization.getVisualization(), PostConstruct.class, activeContext);
parentComposite.layout(true);
} else {
showEmpty(visualizationComposite);
}
} else {
showEmpty(visualizationComposite);
}
}
@Inject
@Optional
void handleSelection(@Named(IServiceConstants.ACTIVE_SELECTION) ISelection selection) {
if (!pinned) {
// ensure that we are able to remove the adapter from a previous selection
EObject previous = null;
if (activeModelElements != null
&& activeModelElements.size() == 1
&& activeModelElements.get(0) instanceof EObject) {
previous = (EObject) activeModelElements.get(0);
}
if (selection instanceof TreeSelection && !selection.isEmpty()) {
TreeSelection s = (TreeSelection) selection;
activeModelElements = s.toList();
activeTypes = getNearestCommonTypes(activeModelElements);
availableModelVisualizations = null;
// remove the adapter from a previous selection
if (previous != null) {
adapterEnabled = false;
previous.eAdapters().remove(updateViewAdapter);
}
if (!activeTypes.isEmpty() && activeModelElements.size() == 1
&& activeModelElements.get(0) instanceof EObject) {
((EObject) activeModelElements.get(0)).eAdapters().add(updateViewAdapter);
adapterEnabled = true;
}
// check if there is a default visualization already configured
showVisualization(!activeTypes.isEmpty() ? lastSelected.get(activeTypes.get(0).getName()) : null); // ???
}
}
}
private List<Class<?>> getNearestCommonTypes(List<?> modelElements) {
if (modelElements.isEmpty()) {
return Collections.emptyList();
}
// get type candidate
Class<? extends Object> class1 = modelElements.get(0).getClass();
Class<?>[] interfaces = class1.getInterfaces();
final Class<?> typeCandidate = interfaces.length > 0 ? interfaces[0] : class1;
// one model element
if (modelElements.size() == 1) {
return Collections.singletonList(typeCandidate);
}
// multiple model elements
boolean sameModelType = modelElements.stream().allMatch(element -> {
Class<? extends Object> elementClass = element.getClass();
Class<?>[] elementInterfaces = elementClass.getInterfaces();
return elementInterfaces.length > 0 && elementInterfaces[0].equals(typeCandidate);
});
// all elements are of the same type
if (sameModelType) {
return Collections.singletonList(typeCandidate);
}
// compute common interfaces
// for class1:
// - compute all interfaces
// - keep interfaces in the same package
// - keep EObject as common super interface
@SuppressWarnings("unchecked")
List<Class<?>> allInterfaces = ClassUtils.getAllInterfaces(class1);
final String name = class1.getPackage().getName();
final String prefix = (name.endsWith(".impl")) ? name.substring(0, name.length() - 5) : name;
allInterfaces.removeIf( i -> ! (i.equals(EObject.class) || i.getPackage().getName().startsWith(prefix)) );
// compute intersection with interfaces of other model elements
for (int i = 1; i < modelElements.size(); i++) {
allInterfaces.retainAll(ClassUtils.getAllInterfaces(modelElements.get(i).getClass()));
}
// remove super interfaces
List<Class<?>> commonInterfaces = new ArrayList<>();
for (Class<?> tmpInterface : allInterfaces) {
List<Class<?>> otherInterfaces = new ArrayList<>(allInterfaces);
otherInterfaces.remove(tmpInterface);
if (otherInterfaces.stream().noneMatch(other -> tmpInterface.isAssignableFrom(other))) {
commonInterfaces.add(tmpInterface);
}
}
return commonInterfaces;
}
/**
*
* @param parent The parent {@link Composite}, should be the {@link #visualizationComposite}.
*/
private void showEmpty(Composite parent) {
Label label = new Label(parent, SWT.NONE);
if (hasActiveModelElement()) {
StringBuilder sb = new StringBuilder("There is no visualization available for the active selection.");
if (activeTypes.isEmpty()) {
sb.append("\n\n - no common types detected - ");
} else {
sb.append("\n\ndetected ");
sb.append((activeModelElements.size() == 1) ? "" : "common " );
sb.append((activeTypes.size() == 1) ? "type:" : "types:" );
activeTypes.forEach(t -> sb.append("\n - " + t.getSimpleName()));
}
label.setText(sb.toString());
} else {
label.setText("There is no active selection.");
}
parent.getParent().layout(true);
}
@PreDestroy
public void preDestroy() {
if (visualizationComposite != null) {
visualizationComposite.dispose();
}
if (activeContext != null) {
activeContext.dispose();
}
}
/**
*
* @return The model type that should be visualized.
*/
public List<Class<?>> getActiveModelTypes() {
return activeTypes;
}
/**
*
* @return The model elements that are used for the visualization.
*/
public List<?> getActiveModelElements() {
return activeModelElements;
}
/**
*
* @return <code>true</code> if an active model element is set,
* <code>false</code> if no active model element is available.
*/
public boolean hasActiveModelElement() {
return activeModelElements != null && !activeModelElements.isEmpty();
}
/**
*
* @return The current active rendered visualization.
*/
public ModelVisualization getActiveVisualization() {
return activeVisualization;
}
/**
*
* @return The list of all available visualizations available for the current
* {@link #activeType}.
*/
public List<ModelVisualization> getAvailableModelVisualizations() {
return availableModelVisualizations != null ? availableModelVisualizations : Collections.emptyList();
}
/**
*
* @return <code>true</code> if the selection listener is disabled,
* <code>false</code> if the visualization is updated on
* selection changes.
*/
public boolean isPinned() {
return pinned;
}
/**
*
* @param pinned <code>true</code> if the selection listener should be disabled,
* <code>false</code> if the visualization should be updated on
* selection changes.
*/
public void setPinned(boolean pinned) {
this.pinned = pinned;
}
/**
* Persist the local state.
*
* @param part The part to which this instance is connected.
*/
@PersistState
public void persistState(MPart part) {
Map<String, String> state = part.getPersistedState();
state.put(LAST_SELECTED_STATE, lastSelected.toString());
}
}