blob: 5d2e162ee239f5d9b0be23b412e0dd222f840349 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2020 Chalmers | University of Gothenburg, rt-labs and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Chalmers | University of Gothenburg and rt-labs - initial API and implementation and/or initial documentation
* Chalmers | University of Gothenburg - additional features, updated API
* Fredrik Johansson and Themistoklis Ntoukolis - initial implementation of the Matrix View
*******************************************************************************/
package org.eclipse.capra.ui.matrix.views;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.eclipse.capra.core.adapters.Connection;
import org.eclipse.capra.core.adapters.TraceMetaModelAdapter;
import org.eclipse.capra.core.adapters.TracePersistenceAdapter;
import org.eclipse.capra.core.handlers.IArtifactHandler;
import org.eclipse.capra.core.handlers.IArtifactUnpacker;
import org.eclipse.capra.core.helpers.ArtifactHelper;
import org.eclipse.capra.core.helpers.EMFHelper;
import org.eclipse.capra.core.helpers.ExtensionPointHelper;
import org.eclipse.capra.core.helpers.TraceHelper;
import org.eclipse.capra.ui.helpers.SelectionSupportHelper;
import org.eclipse.capra.ui.matrix.TraceabilityMatrixBodyToolTip;
import org.eclipse.capra.ui.matrix.TraceabilityMatrixColumnHeaderDataProvider;
import org.eclipse.capra.ui.matrix.TraceabilityMatrixDataProvider;
import org.eclipse.capra.ui.matrix.TraceabilityMatrixHeaderToolTip;
import org.eclipse.capra.ui.matrix.TraceabilityMatrixRowHeaderDataProvider;
import org.eclipse.capra.ui.matrix.selection.TraceabilityMatrixSelectionProvider;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.config.AbstractRegistryConfiguration;
import org.eclipse.nebula.widgets.nattable.config.AbstractUiBindingConfiguration;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.export.command.ExportCommand;
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer;
import org.eclipse.nebula.widgets.nattable.hover.HoverLayer;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.resize.action.ColumnResizeCursorAction;
import org.eclipse.nebula.widgets.nattable.resize.action.RowResizeCursorAction;
import org.eclipse.nebula.widgets.nattable.resize.event.ColumnResizeEventMatcher;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEventMatcher;
import org.eclipse.nebula.widgets.nattable.resize.mode.ColumnResizeDragMode;
import org.eclipse.nebula.widgets.nattable.resize.mode.RowResizeDragMode;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.Style;
import org.eclipse.nebula.widgets.nattable.ui.action.IMouseAction;
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
import org.eclipse.nebula.widgets.nattable.ui.matcher.MouseEventMatcher;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.ViewPart;
/**
* Provides a traceability matrix view, i.e., a tabular view of how the
* different artifacts are related.
* <p>
* The traceability matrix shows all selected artifacts and the artifacts they
* are directly connected to via trace links. It also displays the type of link
* between the artifacts, allows navigating to the artifact by double-clicking
* the column header, and shows information about the artifact and the trace
* link as a tool-tip. It is also possible to export the currently viewed
* traceability matrix as a Microsoft Excel file.
* <p>
* The implementation uses NatTable.
*
* @author Fredrik Johansson
* @author Themistoklis Ntoukolis
* @author Jan-Philipp Steghöfer
*
*/
public class TraceabilityMatrixView extends ViewPart {
/**
* The ID of the view as specified by the extension.
*/
public static final String ID = "org.eclipse.capra.ui.matrix.views.TraceabilityMatrixView";
private static final String SAME_LABEL = "SAME"; // When column header and row header are the same
private static final String LINK_LABEL = "LINKED"; // When there is a link between
private NatTable traceMatrixTable;
private Action refreshAction, showAllAction, exportExcelAction;
private Composite parent;
private ResourceSet resourceSet = new ResourceSetImpl();
private final TraceMetaModelAdapter traceAdapter = ExtensionPointHelper.getTraceMetamodelAdapter().get();
private TraceMetaModelAdapter metamodelAdapter = ExtensionPointHelper.getTraceMetamodelAdapter().get();
private TracePersistenceAdapter persistenceAdapter = ExtensionPointHelper.getTracePersistenceAdapter().get();
private EObject traceModel = null;
private EObject artifactModel = null;
private ArtifactHelper artifactHelper;
private TraceHelper traceHelper;
private TraceabilityMatrixDataProvider bodyDataProvider;
private TraceabilityMatrixSelectionProvider selectionProvider;
private BodyLayerStack bodyLayer;
private List<Object> selectedModels = new ArrayList<>();
private boolean selectionModified = false;
/**
* Listener to detect changes in the selection of other views. Redraws the table
* if necessary.
*/
private ISelectionListener selectionListener = new ISelectionListener() {
@Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
populateSelectedModels(part, selection);
if (traceMatrixTable != null && selectionModified) {
updateTraceabilityMatrix();
}
}
};
/**
* Color configuration. First changing the background color for the labels.
* After that is the coloring for hovering.
*/
private AbstractRegistryConfiguration capraNatTableStyleConfiguration = new AbstractRegistryConfiguration() {
@Override
public void configureRegistry(IConfigRegistry configRegistry) {
// Black background for cells where there should not be any links
Style cellStyle = new Style();
cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_BLACK);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL,
SAME_LABEL);
// Green background for cells where there is a link.
cellStyle = new Style();
cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_GREEN);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL,
LINK_LABEL);
// Style that is applied when cells are hovered
Style style = new Style();
style.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_YELLOW);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, style, DisplayMode.HOVER);
// Style that is applied when selected cells are hovered
style = new Style();
style.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_GREEN);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, style, DisplayMode.SELECT_HOVER);
}
};
/**
* Making it possible to resize columns and rows.
*/
private AbstractUiBindingConfiguration capraUiBindingConfiguration = new AbstractUiBindingConfiguration() {
@Override
public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
uiBindingRegistry.registerFirstMouseMoveBinding(
new ColumnResizeEventMatcher(SWT.NONE, GridRegion.ROW_HEADER, 0), new ColumnResizeCursorAction());
uiBindingRegistry.registerFirstMouseDragMode(
new ColumnResizeEventMatcher(SWT.NONE, GridRegion.ROW_HEADER, 1), new ColumnResizeDragMode());
uiBindingRegistry.registerFirstMouseMoveBinding(
new RowResizeEventMatcher(SWT.NONE, GridRegion.COLUMN_HEADER, 0), new RowResizeCursorAction());
uiBindingRegistry.registerFirstMouseDragMode(
new RowResizeEventMatcher(SWT.NONE, GridRegion.COLUMN_HEADER, 1), new RowResizeDragMode());
// Make the corner on the top left also resizable
uiBindingRegistry.registerFirstMouseMoveBinding(
new ColumnResizeEventMatcher(SWT.NONE, GridRegion.CORNER, 0), new ColumnResizeCursorAction());
uiBindingRegistry.registerFirstMouseDragMode(new ColumnResizeEventMatcher(SWT.NONE, GridRegion.CORNER, 1),
new ColumnResizeDragMode());
uiBindingRegistry.registerFirstMouseMoveBinding(new RowResizeEventMatcher(SWT.NONE, GridRegion.CORNER, 0),
new RowResizeCursorAction());
uiBindingRegistry.registerFirstMouseDragMode(new RowResizeEventMatcher(SWT.NONE, GridRegion.CORNER, 1),
new RowResizeDragMode());
}
};
/**
* Setting labels based on links and later use the labels for customized layout.
*/
private IConfigLabelAccumulator cellLabelAccumulator = new IConfigLabelAccumulator() {
@Override
public void accumulateConfigLabels(LabelStack configLabels, int columnPosition, int rowPosition) {
int columnIndex = bodyLayer.getColumnIndexByPosition(columnPosition);
int rowIndex = bodyLayer.getRowIndexByPosition(rowPosition);
if (sameElement(bodyDataProvider.getRow(rowIndex), bodyDataProvider.getColumn(columnIndex))) {
configLabels.addLabel(SAME_LABEL);
} else {
String cell_text = (String) bodyDataProvider.getDataValue(columnIndex, rowIndex);
if (!cell_text.equals("")) {
configLabels.addLabel(LINK_LABEL);
}
}
}
};
@Override
public void createPartControl(Composite parent) {
this.parent = parent;
updateTraceabilityMatrix();
makeActions();
contributeToActionBars();
// Adding support to react to selections in other views
getViewSite().getPage().addSelectionListener(selectionListener);
}
@Override
public void dispose() {
getViewSite().getPage().removeSelectionListener(selectionListener);
traceMatrixTable.dispose();
traceMatrixTable = null;
super.dispose();
}
@Override
public void setFocus() {
if (traceMatrixTable != null) {
traceMatrixTable.setFocus();
}
}
/**
* Updates the traceability matrix with data from the current selection or with
* data from all traced artifacts if the selection is empty. In essence, it
* disposes of the current table and creates a new one with the current data.
* Also layouts the parent to make sure the table is displayed correctly.
*/
@SuppressWarnings("unchecked")
protected void updateTraceabilityMatrix() {
EObject selectedObject;
List<Connection> traces = new ArrayList<>();
this.artifactModel = persistenceAdapter.getArtifactWrappers(resourceSet);
this.artifactHelper = new ArtifactHelper(this.artifactModel);
this.traceModel = persistenceAdapter.getTraceModel(resourceSet);
this.traceHelper = new TraceHelper(traceModel);
if (!selectedModels.isEmpty()) {
// Show the matrix for the selected objects
for (Object model : selectedModels) {
IArtifactHandler<Object> handler = (IArtifactHandler<Object>) this.artifactHelper.getHandler(model)
.orElse(null);
if (handler != null) {
Object unpackedElement = null;
if (handler instanceof IArtifactUnpacker) {
unpackedElement = IArtifactUnpacker.class.cast(handler).unpack(model);
} else {
unpackedElement = model;
}
EObject wrappedElement = handler.createWrapper(unpackedElement, this.artifactModel);
if (traceHelper.isArtifactInTraceModel(wrappedElement)) {
selectedObject = wrappedElement;
traces.addAll(this.traceAdapter.getConnectedElements(selectedObject, this.traceModel));
}
}
}
} else {
// Without a selection, show a matrix of all traces
if (traceModel != null) {
traces = metamodelAdapter.getAllTraceLinks(traceModel);
}
}
// If the current selection does not contain elements from the trace model, it
// is possible that the list of traces is empty. By catching this case, we only
// update the traceability matrix when there is actually something to show.
if (!traces.isEmpty()) {
if (traceMatrixTable != null) {
traceMatrixTable.dispose();
traceMatrixTable = null;
}
// Creating data providers for body, column and row. The data provider for the
// body provides the data which will be shown in the cells. For columns and
// rows, the labels are created.
this.bodyDataProvider = new TraceabilityMatrixDataProvider(traces, this.traceModel, this.traceAdapter);
IDataProvider colHeaderDataProvider = new TraceabilityMatrixColumnHeaderDataProvider(
this.bodyDataProvider.getColumns(), artifactHelper);
IDataProvider rowHeaderDataProvider = new TraceabilityMatrixRowHeaderDataProvider(
this.bodyDataProvider.getRows(), artifactHelper);
// Putting the data providers to their respective stacks
this.bodyLayer = new BodyLayerStack(this.bodyDataProvider);
ColumnHeaderLayerStack columnHeaderLayer = new ColumnHeaderLayerStack(colHeaderDataProvider);
RowHeaderLayerStack rowHeaderLayer = new RowHeaderLayerStack(rowHeaderDataProvider);
DefaultCornerDataProvider cornerDataProvider = new DefaultCornerDataProvider(colHeaderDataProvider,
rowHeaderDataProvider);
CornerLayer cornerLayer = new CornerLayer(new DataLayer(cornerDataProvider), rowHeaderLayer,
columnHeaderLayer);
// Adding the labels to the body (cells).
bodyLayer.setConfigLabelAccumulator(cellLabelAccumulator);
// Putting all the layers to the grid.
GridLayer gridLayer = new GridLayer(this.bodyLayer, columnHeaderLayer, rowHeaderLayer, cornerLayer);
gridLayer.addConfiguration(capraUiBindingConfiguration);
// Creating the table
traceMatrixTable = new NatTable(parent, gridLayer, false);
// Adding Configuration to the table
traceMatrixTable.addConfiguration(new DefaultNatTableStyleConfiguration());
traceMatrixTable.addConfiguration(this.capraNatTableStyleConfiguration);
traceMatrixTable.configure();
// Attach the selection provider
if (this.selectionProvider == null) {
this.selectionProvider = new TraceabilityMatrixSelectionProvider(bodyLayer.getSelectionLayer(),
this.bodyDataProvider);
} else {
selectionProvider.updateProvider(bodyLayer.getSelectionLayer(), this.bodyDataProvider);
}
getSite().setSelectionProvider(this.selectionProvider);
// Adding the tool tips
attachToolTip();
// Finally, make sure everything is in the right place.
parent.layout();
}
}
// ********************************************************************
/**
* Populates the {@link #selectedModels} property with the objects included in
* the provided selection.
*
* @param part the workbench part whose selection provider is used
* @param selection the selection from which the elements are extracted
*/
private void populateSelectedModels(IWorkbenchPart part, ISelection selection) {
List<Object> newSelectedObjects = SelectionSupportHelper.extractSelectedElements(selection, part);
if (!listEqualsIgnoreOrder(selectedModels, newSelectedObjects)) {
selectionModified = true;
selectedModels.clear();
selectedModels.addAll(newSelectedObjects);
} else {
selectionModified = false;
}
}
/**
* Compares to lists ignoring order and duplicates.
*
* @param <T> the type of the two lists
* @param list1 the first list to compare
* @param list2 the second list to compare
* @return <code>true</code> if the lists are the same, <code>false</code>
* otherwise
*/
private <T> boolean listEqualsIgnoreOrder(List<T> list1, List<T> list2) {
return new HashSet<>(list1).equals(new HashSet<>(list2));
}
/**
* Create the tool tip instances.
*/
private void attachToolTip() {
new TraceabilityMatrixBodyToolTip(this.traceMatrixTable, this.bodyDataProvider, this.artifactHelper);
new TraceabilityMatrixHeaderToolTip(this.traceMatrixTable, this.bodyDataProvider, this.artifactHelper);
}
/**
* Opens an editor for the provided {@code element} based on the URL stored in
* the artifact.
*
* @param element the element for which an editor should be opened
* @throws PartInitException if the editor could not be initialised
*/
private void openEditor(EObject element) throws PartInitException {
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
try {
URI uri = new URI(artifactHelper.getArtifactLocation(element));
IPath artifactPath = new Path(uri.getPath());
artifactPath = artifactPath.removeFirstSegments(1);
IFile artifactFile = ResourcesPlugin.getWorkspace().getRoot().getFile(artifactPath);
IDE.openEditor(page, artifactFile);
} catch (URISyntaxException e) {
}
}
private void contributeToActionBars() {
IActionBars bars = getViewSite().getActionBars();
fillLocalPullDown(bars.getMenuManager());
fillLocalToolBar(bars.getToolBarManager());
}
private void fillLocalPullDown(IMenuManager manager) {
manager.add(refreshAction);
manager.add(showAllAction);
manager.add(new Separator());
manager.add(exportExcelAction);
}
private void fillLocalToolBar(IToolBarManager manager) {
manager.add(refreshAction);
manager.add(showAllAction);
}
private void makeActions() {
refreshAction = new Action() {
@Override
public void run() {
updateTraceabilityMatrix();
}
};
refreshAction.setText("Refresh");
refreshAction.setToolTipText("Refresh");
showAllAction = new Action() {
@Override
public void run() {
selectedModels.clear();
updateTraceabilityMatrix();
}
};
showAllAction.setText("Show all");
showAllAction.setToolTipText("Show all");
exportExcelAction = new Action() {
@Override
public void run() {
ExportCommand cmd = new ExportCommand(traceMatrixTable.getConfigRegistry(),
traceMatrixTable.getShell());
traceMatrixTable.doCommand(cmd);
}
};
exportExcelAction.setText("Export to Excel...");
exportExcelAction.setToolTipText("Exports the currently shown traceability matrix as an Excel file.");
}
private boolean sameElement(EObject first, EObject second) {
return EMFHelper.getIdentifier(first).equals(EMFHelper.getIdentifier(second));
}
/**
* Class for the column stack
*/
private class ColumnHeaderLayerStack extends AbstractLayerTransform {
public ColumnHeaderLayerStack(IDataProvider dataProvider) {
DataLayer dataLayer = new DataLayer(dataProvider);
ColumnHeaderLayer colHeaderLayer = new ColumnHeaderLayer(dataLayer, TraceabilityMatrixView.this.bodyLayer,
TraceabilityMatrixView.this.bodyLayer.getSelectionLayer());
// Adding double click to the column header
colHeaderLayer.addConfiguration(new AbstractUiBindingConfiguration() {
@Override
public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
uiBindingRegistry.registerDoubleClickBinding(MouseEventMatcher.columnHeaderLeftClick(0),
new IMouseAction() {
@Override
public void run(NatTable natTable, MouseEvent event) {
try {
int columnPosition = natTable.getColumnPositionByX(event.x);
int columnIndex = natTable.getColumnIndexByPosition(columnPosition);
EObject element = bodyDataProvider.getColumn(columnIndex);
openEditor(element);
} catch (PartInitException e) {
// Deliberately do nothing.
}
}
});
}
});
setUnderlyingLayer(colHeaderLayer);
}
}
/**
* Class for the row stack
*/
private class RowHeaderLayerStack extends AbstractLayerTransform {
public RowHeaderLayerStack(IDataProvider dataProvider) {
DataLayer dataLayer = new DataLayer(dataProvider, 50, 20);
RowHeaderLayer rowHeaderLayer = new RowHeaderLayer(dataLayer, TraceabilityMatrixView.this.bodyLayer,
TraceabilityMatrixView.this.bodyLayer.getSelectionLayer());
// Adding double click to the row header
rowHeaderLayer.addConfiguration(new AbstractUiBindingConfiguration() {
@Override
public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
uiBindingRegistry.registerDoubleClickBinding(MouseEventMatcher.rowHeaderLeftClick(0),
new IMouseAction() {
@Override
public void run(NatTable natTable, MouseEvent event) {
try {
int rowPosition = natTable.getRowPositionByY(event.y);
int rowIndex = natTable.getRowIndexByPosition(rowPosition);
EObject element = bodyDataProvider.getRow(rowIndex);
openEditor(element);
} catch (PartInitException e) {
// Deliberately do nothing.
}
}
});
}
});
setUnderlyingLayer(rowHeaderLayer);
}
}
/**
* Class for the body stack
*/
private class BodyLayerStack extends AbstractLayerTransform {
private SelectionLayer selectionLayer;
public BodyLayerStack(IDataProvider dataProvider) {
DataLayer bodyDataLayer = new DataLayer(dataProvider);
HoverLayer bodyHoverLayer = new HoverLayer(bodyDataLayer);
this.selectionLayer = new SelectionLayer(bodyHoverLayer);
ViewportLayer viewportLayer = new ViewportLayer(this.selectionLayer);
setUnderlyingLayer(viewportLayer);
}
public SelectionLayer getSelectionLayer() {
return this.selectionLayer;
}
}
}