| /********************************************************************************* |
| * 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()); |
| } |
| |
| } |