blob: 37f88b5a1fc7f0cc91c9f3aefe50708fe04ba630 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2018 EclipseSource Muenchen GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stefan Dirix - initial API and implementation
* Philip Langer - introduce caching and improve selection, bug 527567
*******************************************************************************/
package org.eclipse.papyrus.compare.diagram.ide.ui.contentmergeviewer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.ide.ui.internal.configuration.EMFCompareConfiguration;
import org.eclipse.emf.compare.ide.ui.internal.contentmergeviewer.tree.TreeContentMergeViewer;
import org.eclipse.emf.compare.ide.ui.internal.contentmergeviewer.tree.provider.MergeViewerItemProviderConfiguration;
import org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer.actions.MergeAction;
import org.eclipse.emf.compare.internal.merge.MergeMode;
import org.eclipse.emf.compare.rcp.EMFCompareRCPPlugin;
import org.eclipse.emf.compare.rcp.ui.contentmergeviewer.accessor.ICompareAccessor;
import org.eclipse.emf.compare.rcp.ui.internal.mergeviewer.impl.AbstractMergeViewer;
import org.eclipse.emf.compare.rcp.ui.internal.mergeviewer.impl.AbstractTableOrTreeMergeViewer.ElementComparer;
import org.eclipse.emf.compare.rcp.ui.internal.mergeviewer.impl.TreeMergeViewer;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.IMergeViewer.MergeViewerSide;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.item.IMergeViewerItem;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.item.provider.IMergeViewerItemProviderConfiguration;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.papyrus.compare.diagram.ide.ui.contentmergeviewer.facet.PapyrusFacetContentProviderWrapperAdapterFactory;
import org.eclipse.papyrus.compare.diagram.ide.ui.contentmergeviewer.provider.PapyrusTreeContentMergeViewerItemLabelProvider;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.widgets.Composite;
/**
* Specialized Tree Content Merge Viewer for Papyrus.
*
* @author Stefan Dirix
*/
@SuppressWarnings("restriction")
public class PapyrusTreeContentMergeViewer extends TreeContentMergeViewer {
/**
* Since Papyrus trees could in theory be infinite, we need a maximum search level.
*/
private static final int MAX_SEARCH_LEVEL = 20;
/**
* Map of objects to {@link PapyrusContentProviderMergeViewerItem Papyrus merge viewer items} used when
* changing the selection in order to find the merge viewer item to be selected when a specific object
* (model element or diff) is to be revealed.
*/
private Map<Object, IMergeViewerItem> cachedMapForSelection;
/**
* A queue of computations for computing the content tree items in the {@linked #getLeftMergeViewer()
* right merge viewer}.
*/
private LinkedList<AbstractContentFunction> leftContentComputations;
/**
* A queue of computations for computing the content tree items in the {@linked #getRightMergeViewer()
* left merge viewer}.
*/
private LinkedList<AbstractContentFunction> rightContentComputations;
/**
* Constructor.
*
* @param style
* the style parameter
* @param bundle
* the {@link ResourceBundle}
* @param parent
* the {@link Composite} parent
* @param config
* the {@link EMFCompareConfiguration}
*/
public PapyrusTreeContentMergeViewer(int style, ResourceBundle bundle, Composite parent,
EMFCompareConfiguration config) {
super(style, bundle, parent, config);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.ide.ui.internal.contentmergeviewer.EMFCompareContentMergeViewer#createMergeViewer(org.eclipse.swt.widgets.Composite)
*/
@Override
protected AbstractMergeViewer createMergeViewer(final Composite parent, final MergeViewerSide side) {
final TreeMergeViewer mergeTreeViewer = new TreeMergeViewer(parent, side, this,
getCompareConfiguration()) {
@Override
protected IAction createAction(MergeMode mode, Diff diff) {
return new MergeAction(getCompareConfiguration(),
EMFCompareRCPPlugin.getDefault().getMergerRegistry(), mode, null,
new StructuredSelection(diff));
}
};
final IContentProvider contentProvider = createMergeViewerContentProvider(side);
mergeTreeViewer.setContentProvider(contentProvider);
final IBaseLabelProvider labelProvider = new PapyrusTreeContentMergeViewerItemLabelProvider(
getResourceBundle(), getAdapterFactory(), side);
mergeTreeViewer.setLabelProvider(labelProvider);
hookListeners(mergeTreeViewer);
return mergeTreeViewer;
}
@Override
protected IMergeViewerItemProviderConfiguration createMergeViewerItemProviderConfiguration(
MergeViewerSide side) {
ComposedAdapterFactory factory = new ComposedAdapterFactory(new AdapterFactory[] {
new PapyrusFacetContentProviderWrapperAdapterFactory(), getAdapterFactory(), });
return new MergeViewerItemProviderConfiguration(factory, getDifferenceGroupProvider(),
getDifferenceFilterPredicate(), getCompareConfiguration().getComparison(), side);
}
/**
* Check whether the given input is an instance of {@link ICompareAccessor}.
*
* @param input
* the given input to check
* @return {@code true}, if the input is an instance of {@link ICompareAccessor}, {@code false} otherwise
*/
private static boolean isCompareAccessor(Object input) {
return input instanceof ICompareAccessor;
}
/**
* Check whether the current input of the given side differs from the given one.
*
* @param side
* the side to be checked, either {@link MergeViewerSide#LEFT} or {@link MergeViewerSide#RIGHT}
* @param input
* the input to check against
* @return {@code true}, if the input is different, {@code false} otherwise
*/
private boolean isDifferentInput(MergeViewerSide side, Object input) {
TreeMergeViewer viewer = getMergeViewer(side);
if (!isCompareAccessor(input) || !isCompareAccessor(viewer.getInput())) {
return true;
}
ImmutableList<? extends IMergeViewerItem> inputItems = ICompareAccessor.class.cast(input).getItems();
ImmutableList<? extends IMergeViewerItem> vieweritems = ICompareAccessor.class.cast(viewer.getInput())
.getItems();
if (inputItems.size() != vieweritems.size()) {
return true;
}
ElementComparer comparer = new ElementComparer();
for (int i = 0; i < vieweritems.size(); i++) {
if (!comparer.equals(inputItems.get(i), vieweritems.get(i))) {
return true;
}
}
return false;
}
/**
* Returns the {@link TreeMergeViewer} of the given side.
*
* @param side
* the side for which to return the {@link TreeMergeViewer}
* @return the {@link TreeMergeViewer} of the respective side
*/
public TreeMergeViewer getMergeViewer(MergeViewerSide side) {
if (side == MergeViewerSide.LEFT) {
return getLeftMergeViewer();
} else if (side == MergeViewerSide.RIGHT) {
return getRightMergeViewer();
}
return getAncestorMergeViewer();
}
@Override
protected void updateContent(Object ancestor, Object left, Object right) {
// Modify selection so it works with the Papyrus Merge Viewer Items
// first check whether the input on any side has changed
if (isDifferentInput(MergeViewerSide.LEFT, left) || isDifferentInput(MergeViewerSide.RIGHT, right)) {
getAncestorMergeViewer().setInput(ancestor);
getLeftMergeViewer().setInput(left);
getRightMergeViewer().setInput(right);
leftContentComputations = null;
rightContentComputations = null;
cachedMapForSelection = null;
} else {
getLeftMergeViewer().refresh();
getRightMergeViewer().refresh();
}
IMergeViewerItem leftInitialItem = null;
if (left instanceof ICompareAccessor) {
leftInitialItem = ((ICompareAccessor)left).getInitialItem();
}
// Bug 458818: In some cases, the left initial item is null because
// the item that should be selected has been deleted on the right
// and this delete is part of a conflict
if (leftInitialItem == null || leftInitialItem.getLeft() == null) {
if (right instanceof ICompareAccessor) {
IMergeViewerItem rightInitialItem = ((ICompareAccessor)right).getInitialItem();
if (rightInitialItem == null) {
getLeftMergeViewer().setSelection(StructuredSelection.EMPTY, true);
} else {
setSelection((ICompareAccessor)right);
}
} else {
// Strange case: left is an ICompareAccessor but right is not?
getLeftMergeViewer().setSelection(StructuredSelection.EMPTY, true);
}
} else {
// others will synchronize on this one :)
setSelection((ICompareAccessor)left);
}
redrawCenterControl();
}
/**
* Sets the selection according to the accessor.
*
* @param accessor
* The {@link ICompareAccessor} which contains the root tree elements and the initial
* selection.
*/
private void setSelection(ICompareAccessor accessor) {
// First try to set the initial item directly
final IMergeViewerItem initialItem = accessor.getInitialItem();
TreeMergeViewer viewer = getMergeViewer(initialItem.getSide());
viewer.setSelection(new StructuredSelection(initialItem), true);
// if that didn't work (empty selection), use cache to find correct merge viewer item
if (viewer.getSelection().isEmpty()) {
IMergeViewerItem itemToBeSelected = getItemToBeSelected(initialItem, viewer);
if (itemToBeSelected != null) {
viewer.setSelection(new StructuredSelection(itemToBeSelected), true);
} else {
viewer.setSelection(new StructuredSelection(), true);
}
}
}
/**
* Determines the item to select from the corresponding side viewer, given an input item.
*
* @param item
* the input item.
* @param viewer
* the viewer in which to select the item.
* @return the item to be selected.
*/
private IMergeViewerItem getItemToBeSelected(IMergeViewerItem item, TreeMergeViewer viewer) {
MergeViewerSide side = viewer.getSide();
Object itemValue = item.getSideValue(side);
IMergeViewerItem result;
if (cachedMapForSelection == null) {
cachedMapForSelection = Maps.newHashMap();
result = null;
} else {
result = cachedMapForSelection.get(itemValue);
}
if (result == null && itemValue != null) {
LinkedList<AbstractContentFunction> contentComputations;
switch (side) {
case LEFT:
if (leftContentComputations == null) {
leftContentComputations = Lists.newLinkedList();
leftContentComputations.add(new ElementFunction(side, viewer, leftContentComputations,
cachedMapForSelection));
}
contentComputations = leftContentComputations;
break;
case RIGHT:
if (rightContentComputations == null) {
rightContentComputations = Lists.newLinkedList();
rightContentComputations.add(new ElementFunction(side, viewer,
rightContentComputations, cachedMapForSelection));
}
contentComputations = rightContentComputations;
break;
default:
throw new IllegalArgumentException();
}
Collection<Object> containers = getContainers(itemValue);
if (!containers.isEmpty()) {
// move necessary computations to the front
List<AbstractContentFunction> containerComputations = extractComputations(contentComputations,
containers);
contentComputations.addAll(0, containerComputations);
}
result = computeItemToBeSelected(itemValue, containers, contentComputations);
}
return result;
}
/**
* Computes the item to be selected for the given item value, taking into account the containers, using
* the content computations.
*
* @param itemValue
* the item for which to base the selection.
* @param containers
* the containers for that item.
* @param contentComputations
* the computations for computing content.
* @return the item to be selected.
*/
private IMergeViewerItem computeItemToBeSelected(Object itemValue, Collection<Object> containers,
LinkedList<AbstractContentFunction> contentComputations) {
// We try to limit the amount of time we spend looking for selection.
// Sometimes the object will not be present in the tree at all,
// causing the whole tree to be visited.
// So we keep track of which of the contains have not been visited,
// removing any containers for which there is already a selection computed.
IMergeViewerItem result = null;
Collection<Object> unvistedContainers = Sets.newHashSet(containers);
unvistedContainers.removeAll(cachedMapForSelection.keySet());
long start = System.currentTimeMillis();
while (!contentComputations.isEmpty()) {
AbstractContentFunction contentFunction = contentComputations.removeFirst();
// If we've visited all the containers and have been computing for more the 2 seconds,
// stop looking and select the container instead.
unvistedContainers.remove(contentFunction.getValue());
if (unvistedContainers.isEmpty() && (System.currentTimeMillis() - start) > 2 * 1000) {
break;
}
result = contentFunction.apply(itemValue, containers);
if (result != null) {
break;
}
}
// If we haven't found the selection, try to find a selection for the nearest container.
if (result == null) {
for (Object container : containers) {
result = cachedMapForSelection.get(container);
if (result != null) {
break;
}
}
}
return result;
}
@Override
protected void handleDispose(DisposeEvent event) {
if (cachedMapForSelection != null) {
cachedMapForSelection = null;
}
super.handleDispose(event);
}
/**
* Extracts all content functions for the given objects from the computation list.
*
* @param computations
* computation list from which functions are extracted.
* @param values
* values specifying which functions should be extracted.
* @return Non-null list of computations covering the given objects.
*/
private List<AbstractContentFunction> extractComputations(
LinkedList<AbstractContentFunction> computations, Collection<Object> values) {
List<AbstractContentFunction> extractedComputations = new ArrayList<>();
Iterator<AbstractContentFunction> computationsIterator = computations.iterator();
while (computationsIterator.hasNext()) {
AbstractContentFunction contentFuction = computationsIterator.next();
if (values.contains(contentFuction.getValue())) {
computationsIterator.remove();
extractedComputations.add(contentFuction);
}
}
return extractedComputations;
}
/**
* Returns all EObject containers for the given value, if the given object is an EObject.
*
* @param eObject
* eObject value.
* @return Non-null collection of containers.
*/
private Collection<Object> getContainers(Object eObject) {
Collection<Object> containers;
if (eObject instanceof EObject) {
containers = Sets.newLinkedHashSet();
for (EObject container = ((EObject)eObject).eContainer(); container != null; container = container
.eContainer()) {
containers.add(container);
}
} else {
containers = Collections.emptySet();
}
return containers;
}
/**
* A function for computing the items in the content tree induced by an item provider. Instances are
* managed in a {@link #contentComputations} queue and are processed to update the {@linked #selections
* selections} cache.
*
* @see PapyrusTreeContentMergeViewer#getItemToBeSelected(IMergeViewerItem)
*/
private abstract static class AbstractContentFunction {
/**
* The side for which the computation is being done.
*/
protected final MergeViewerSide side;
/**
* The tree content provider which induces the content tree.
*/
protected final ITreeContentProvider provider;
/**
* The queue of computations being managed.
*
* @see PapyrusTreeContentMergeViewer#leftContentComputations
* @see PapyrusTreeContentMergeViewer#rightContentComputations
*/
protected final LinkedList<AbstractContentFunction> contentComputations;
/**
* The selections being cached.
*
* @see PapyrusTreeContentMergeViewer#cachedMapForSelection
*/
protected final Map<Object, IMergeViewerItem> selections;
/**
* Creates and instance for a given side that induces content based on the given providers and manages
* the queue of content computations, updating the selections.
*
* @param side
* the side for which the content is being computed.
* @param provider
* the provider used to induce content.
* @param contentComputations
* the queue of computations being managed.
* @param selections
* the selection cache being managed.
*/
protected AbstractContentFunction(MergeViewerSide side, ITreeContentProvider provider,
LinkedList<AbstractContentFunction> contentComputations,
Map<Object, IMergeViewerItem> selections) {
this.side = side;
this.provider = provider;
this.contentComputations = contentComputations;
this.selections = selections;
}
/**
* Computes new content and if any of the new content item matches the given value, returns the
* corresponding item from the side viewer. The {@link #contentComputations queue} is updated with
* functions for the new content items. Any new item with a value that matches one of the containers
* is placed at the front of the queue, rather than the back.
*
* @param matchValue
* the value to match.
* @param containers
* the items that are of high priority to process first.
* @return the corresponding side viewer item for the match value, or null.
*/
public abstract IMergeViewerItem apply(Object matchValue, Collection<?> containers);
/**
* Returns the value for which content will be computed.
*
* @return the value for which content will be computed.
*/
public abstract Object getValue();
/**
* A base helper method that does the computation needed by {@link #apply(Object, Collection)}.
*
* @param matchValue
* the value to be matched.
* @param containers
* the items that are of high priority to process first.
* @param values
* the new content items to be processed.
* @param depth
* the current tree depth of the traversal.
* @return the corresponding side viewer item for the match value, or null.
*/
protected IMergeViewerItem apply(Object matchValue, Collection<?> containers, Object[] values,
int depth) {
IMergeViewerItem result = null;
for (Object element : values) {
if (element instanceof IMergeViewerItem) {
final IMergeViewerItem item = (IMergeViewerItem)element;
Object sideValue = item.getSideValue(side);
// If we don't yet have a result, and the new value is equal to the matching value, then
// cache it as the result.
if (result == null) {
if (sideValue != null && sideValue.equals(matchValue)) {
result = item;
}
}
// Create a new child function for the given item and add it to the front or the back of
// the queue depending on whether the high priority containers include the item's side
// value. So in general the content is build breadth first, but the subtree of the
// containers is processed depth first.
ChildFunction childFunction = new ChildFunction(side, provider, item, depth,
contentComputations, selections);
if (containers.contains(sideValue)) {
contentComputations.addFirst(childFunction);
} else {
contentComputations.addLast(childFunction);
}
}
}
return result;
}
}
/**
* A content function that computes the root elements of a side viewer.
*/
private static final class ElementFunction extends AbstractContentFunction {
/**
* The input object of the side viewer.
*/
private final Object input;
/**
* Creates and instance for a given side viewer that induces content based on the viewer's provider
* and manages the queue of content computations, updating the selections.
*
* @param side
* the side for which the content is being computed.
* @param viewer
* the side viewer for which to induce content.
* @param contentComputations
* the queue of computations being managed.
* @param selections
* the selection cache being managed.
*/
private ElementFunction(MergeViewerSide side, TreeMergeViewer viewer,
LinkedList<AbstractContentFunction> contentComputations,
Map<Object, IMergeViewerItem> selections) {
super(side, (ITreeContentProvider)viewer.getContentProvider(), contentComputations, selections);
this.input = viewer.getInput();
}
@Override
public Object getValue() {
return input;
}
@Override
public IMergeViewerItem apply(Object matchValue, Collection<?> containers) {
return apply(matchValue, containers, provider.getElements(input), 0);
}
}
/**
* A content function that computes the child elements of a side viewer.
*/
private static final class ChildFunction extends AbstractContentFunction {
/**
* The item for which to induce child content.
*/
private final IMergeViewerItem item;
/**
* The depth of the item.
*/
private final int depth;
/**
* Creates and instance for a given side viewer's content provider that induces for the given item and
* the given depth and manages the queue of content computations, updating the selections.
*
* @param side
* the side for which the content is being computed.
* @param provider
* the content provider of the side viewer.
* @param item
* the item for which to induce child content.
* @param depth
* the depth of the item.
* @param contentComputations
* the queue of computations being managed.
* @param selections
* the selection cache being managed.
*/
private ChildFunction(MergeViewerSide side, ITreeContentProvider provider, IMergeViewerItem item,
int depth, LinkedList<AbstractContentFunction> contentComputations,
Map<Object, IMergeViewerItem> selections) {
super(side, provider, contentComputations, selections);
this.item = item;
this.depth = depth;
Object value = item.getSideValue(side);
if (value != null) {
selections.put(value, item);
}
}
@Override
public Object getValue() {
return item.getSideValue(side);
}
@Override
public IMergeViewerItem apply(Object matchValue, Collection<?> containers) {
if (depth == MAX_SEARCH_LEVEL) {
return null;
} else {
return apply(matchValue, containers, provider.getChildren(item), depth + 1);
}
}
}
}