/*******************************************************************************
* Copyright (c) 2013, 2015 Obeo.
* 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:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer;
import static com.google.common.base.Predicates.instanceOf;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.Assert;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.ide.ui.internal.util.JFaceUtil;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.ICompareColor;
import org.eclipse.emf.edit.tree.TreeNode;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
/**
* A specific canvas that must be presented next to a TreeViewer. It shows consequences of a Diff (required
* and unmergeable differences), as in the TreeViewer, but in a compact format (small colored rectangles) and
* with links to respectives Treeitems.
*
* @author <a href="mailto:axel.richard@obeo.fr">Axel Richard</a>
*/
public class EMFCompareDiffTreeRuler extends Canvas {
/** The vertical offset for an annotation. */
private static final int Y_OFFSET = 6;
/** The height of an annotation. */
private static final int ANNOTATION_HEIGHT = 5;
/** The TreeViewer associated with this Treeruler. */
private final WrappableTreeViewer fTreeViewer;
/**
* A map that links a rectangle with a tree item and tree node couple. <b>Note</b> that the TreeNode might
* not be the actual node represented by the TreeItem of its {@link TreeNodeToVisibleTreeItem} couple: it
* could be a child if the TreeItem is not expanded (hence the node's real TreeItem is hidden from view,
* or might not even exist yet in the Tree).
*/
private Map<Rectangle, TreeNodeToVisibleTreeItem> annotationsData;
/** The paint listener. */
private PaintListener paintListener;
/** The mouse click listener. */
private MouseListener mouseClickListener;
/** The mouse move listener. */
private MouseMoveListener mouseMoveListener;
/** The mouse track listener. */
private MouseTrackListener mouseTrackListener;
/** The last cursor used. */
private Cursor lastCursor;
private final DependencyData dependencyData;
private ICompareColor compareColor;
/**
* Constructor.
*
* @param parent
* the control's parent.
* @param style
* the style of the control to construct.
* @param width
* the control's width.
* @param treeViewer
* the TreeViewer associated with this control.
* @param config
* the configuration for this control.
*/
EMFCompareDiffTreeRuler(Composite parent, int style, WrappableTreeViewer treeViewer,
DependencyData dependencyData, ICompareColor compareColor) {
super(parent, style);
fTreeViewer = treeViewer;
this.dependencyData = dependencyData;
this.compareColor = compareColor;
annotationsData = Maps.newHashMap();
paintListener = new PaintListener() {
public void paintControl(PaintEvent e) {
handlePaintEvent(e);
}
};
addPaintListener(paintListener);
mouseClickListener = new MouseListener() {
public void mouseUp(MouseEvent e) {
handleMouseClickEvent(e);
}
public void mouseDown(MouseEvent e) {
// Do nothing.
}
public void mouseDoubleClick(MouseEvent e) {
// Do nothing.
}
};
addMouseListener(mouseClickListener);
mouseMoveListener = new MouseMoveListener() {
public void mouseMove(MouseEvent e) {
handleMouveMoveEvent(e);
}
};
addMouseMoveListener(mouseMoveListener);
mouseTrackListener = new MouseTrackListener() {
public void mouseHover(MouseEvent e) {
handleMouseHoverEvent(e);
}
public void mouseExit(MouseEvent e) {
// Do nothing.
}
public void mouseEnter(MouseEvent e) {
// Do nothing.
}
};
addMouseTrackListener(mouseTrackListener);
}
/**
* Handles the dispose event on this control.
*/
public void handleDispose() {
removeMouseTrackListener(mouseTrackListener);
removeMouseMoveListener(mouseMoveListener);
removeMouseListener(mouseClickListener);
removePaintListener(paintListener);
}
/**
* Handles the paint event.
*
* @param e
* the paint event.
*/
private void handlePaintEvent(PaintEvent e) {
annotationsData.clear();
for (Diff diff : dependencyData.getRequires()) {
final Collection<TreeNode> treeNodes = dependencyData.getTreeNodes(diff);
for (TreeNodeToVisibleTreeItem treeNodeToVisibleTreeItem : findTreeItems(treeNodes)) {
if (!JFaceUtil.isFiltered(fTreeViewer, treeNodeToVisibleTreeItem.getTreeNode(), null)) {
createAnnotation(e, treeNodeToVisibleTreeItem, compareColor.getRequiredFillColor(),
compareColor.getRequiredStrokeColor());
}
}
}
for (Diff diff : dependencyData.getRejections()) {
final Collection<TreeNode> treeNodes = dependencyData.getTreeNodes(diff);
for (TreeNodeToVisibleTreeItem treeNodeToVisibleTreeItem : findTreeItems(treeNodes)) {
if (!JFaceUtil.isFiltered(fTreeViewer, treeNodeToVisibleTreeItem.getTreeNode(), null)) {
createAnnotation(e, treeNodeToVisibleTreeItem, compareColor.getUnmergeableFillColor(),
compareColor.getUnmergeableStrokeColor());
}
}
}
}
public Collection<TreeNodeToVisibleTreeItem> findTreeItems(Collection<TreeNode> treeNodes) {
final Set<TreeNodeToVisibleTreeItem> result = new LinkedHashSet<TreeNodeToVisibleTreeItem>();
for (TreeNode node : treeNodes) {
final TreeItem item = findFirstAncestorTreeItem(node);
if (item != null) {
result.add(new TreeNodeToVisibleTreeItem(node, item));
}
}
return result;
}
private TreeItem findFirstAncestorTreeItem(TreeNode node) {
CompareInputAdapter cia = findFirstAncestorCompareInputAdapter(node);
Object item;
if (cia != null) {
item = fTreeViewer.testFindItem(cia);
} else {
item = null;
}
TreeNode parent = node;
while (!(item instanceof TreeItem) && (parent = parent.getParent()) != null) {
cia = findFirstAncestorCompareInputAdapter(parent);
if (cia != null) {
item = fTreeViewer.testFindItem(cia);
} else {
item = null;
}
}
if (item instanceof TreeItem) {
return (TreeItem)item;
} else {
return null;
}
}
private CompareInputAdapter findFirstAncestorCompareInputAdapter(TreeNode node) {
CompareInputAdapter cia = getCompareInputAdapter(node);
if (cia != null) {
return cia;
}
TreeNode parent = node;
while (cia == null && (parent = parent.getParent()) != null) {
cia = getCompareInputAdapter(parent);
}
return cia;
}
private CompareInputAdapter getCompareInputAdapter(TreeNode node) {
return (CompareInputAdapter)Iterators
.tryFind(node.eAdapters().iterator(), instanceOf(CompareInputAdapter.class)).orNull();
}
/**
* Handles the mouse click event.
*
* @param e
* the mouse click event.
*/
private void handleMouseClickEvent(MouseEvent e) {
for (Map.Entry<Rectangle, TreeNodeToVisibleTreeItem> entry : annotationsData.entrySet()) {
if (e.y >= entry.getKey().y && e.y <= entry.getKey().y + ANNOTATION_HEIGHT) {
TreeItem targetItem = createChildrenDownTo(entry.getValue().getTreeItem(),
entry.getValue().getTreeNode());
TreePath treePath = getTreePathFromItem(targetItem);
fTreeViewer.expandToLevel(treePath, 0);
fTreeViewer.reveal(treePath);
if (isVerticalScrollBarEnabled()) {
TreeItem previousItem = getPreviousItem(targetItem, 2);
fTreeViewer.getTree().setTopItem(previousItem);
}
redraw();
return;
}
}
}
private TreeItem createChildrenDownTo(TreeItem from, TreeNode to) {
TreeItem currentItem = from;
TreeNode currentNode = getTreeNodeFromAdapter(currentItem.getData());
final Iterator<TreeNode> pathToTarget = getPath(currentNode, to).iterator();
while (currentNode != to && pathToTarget.hasNext()) {
// pathToTarget excludes "from" so we can safely iterate it after expanding "currentItem"
TreeItem[] children = currentItem.getItems();
if (isDummyChild(children)) {
fTreeViewer.createChildren(currentItem);
children = currentItem.getItems();
}
currentNode = pathToTarget.next();
for (TreeItem child : children) {
TreeNode childNode = getTreeNodeFromAdapter(child.getData());
if (childNode == currentNode) {
currentItem = child;
break;
}
}
}
return currentItem;
}
private TreeNode getTreeNodeFromAdapter(Object data) {
if (data instanceof Adapter) {
Notifier target = ((Adapter)data).getTarget();
if (target instanceof TreeNode) {
return (TreeNode)target;
}
}
return null;
}
/* excludes "from" */
private Iterable<TreeNode> getPath(TreeNode from, TreeNode to) {
if (to == from) {
return Collections.emptyList();
}
final List<TreeNode> path = new ArrayList<TreeNode>();
path.add(to);
TreeNode parent = to.getParent();
while (parent != null && parent != from) {
path.add(parent);
parent = parent.getParent();
}
return Lists.reverse(path);
}
private boolean isDummyChild(TreeItem[] items) {
// item with non created children has a fake child item with null data.
return items.length == 1 && items[0].getData() == null;
}
/**
* Handles the mouse move event.
*
* @param e
* the mouse move event.
*/
private void handleMouveMoveEvent(MouseEvent e) {
Cursor cursor = null;
for (Rectangle rect : annotationsData.keySet()) {
if (e.y >= rect.y && e.y <= rect.y + ANNOTATION_HEIGHT) {
cursor = e.display.getSystemCursor(SWT.CURSOR_HAND);
break;
}
}
if (cursor != lastCursor) {
setCursor(cursor);
lastCursor = cursor;
}
}
/**
* Handles the mouse hover event.
*
* @param e
* the mouve hover event.
*/
private void handleMouseHoverEvent(MouseEvent e) {
String overview = ""; //$NON-NLS-1$
for (Map.Entry<Rectangle, TreeNodeToVisibleTreeItem> entry : annotationsData.entrySet()) {
if (e.y >= entry.getKey().y && e.y <= entry.getKey().y + ANNOTATION_HEIGHT) {
TreeNode node = entry.getValue().getTreeNode();
// TODO is there a chance the label provider won't be an instance of that low-level one?
if (fTreeViewer.getLabelProvider() instanceof DelegatingStyledCellLabelProvider) {
overview = ((DelegatingStyledCellLabelProvider)fTreeViewer.getLabelProvider())
.getStyledStringProvider().getStyledText(node).toString();
}
break;
}
}
setToolTipText(overview);
}
/**
* Create an annotation in the tree ruler.
*
* @param e
* the PaintEvent.
* @param nodeToItem
* the visible item on which to add this annotation.
* @param fill
* the annotation's fill color.
* @param border
* the annotation's border color.
*/
private void createAnnotation(PaintEvent e, TreeNodeToVisibleTreeItem treeNodeToVisibleTreeItem,
Color fill, Color border) {
TreeItem item = getDeepestVisibleTreeItem(treeNodeToVisibleTreeItem.getTreeItem(),
treeNodeToVisibleTreeItem.getTreeItem());
if (item != null) {
int y = item.getBounds().y;
int yRuler = getSize().y;
if (isVerticalScrollBarEnabled()) {
int yMin = Math.abs(item.getParent().getItems()[0].getBounds().y);
int yMax = getLastVisibleItem().getBounds().y;
int realYMax = yMax + yMin;
if (realYMax > 0) {
y = (y + yMin) * yRuler / realYMax;
} else {
y = (y + yMin) * yRuler;
}
if (y + Y_OFFSET + ANNOTATION_HEIGHT > yRuler) {
y = yRuler - Y_OFFSET - ANNOTATION_HEIGHT;
}
}
Rectangle rect = drawAnnotation(e.gc, 2, y + Y_OFFSET, getBounds().width - 5, ANNOTATION_HEIGHT,
fill, border);
annotationsData.put(rect, treeNodeToVisibleTreeItem);
}
}
/**
* Returns the full tree path of the given tree item.
*
* @param item
* the given tree item.
* @return the full tree path of the given tree item.
*/
private TreePath getTreePathFromItem(TreeItem item) {
LinkedList<Object> segments = Lists.newLinkedList();
TreeItem parent = item;
while (parent != null) {
Object segment = parent.getData();
Assert.isNotNull(segment);
segments.addFirst(segment);
parent = parent.getParentItem();
}
return new TreePath(segments.toArray());
}
/**
* Checks if the vertical scroll bar of the tree viewer associated with this tree ruler is activated and
* enabled.
*
* @return true if the vertical scroll bar is activated and enabled, false otherwise.
*/
private boolean isVerticalScrollBarEnabled() {
ScrollBar verticalBar = fTreeViewer.getTree().getVerticalBar();
if (verticalBar != null) {
return verticalBar.isVisible() && verticalBar.isEnabled();
}
return false;
}
/**
* Draw an annotation (a Rectangle) on this tree ruler.
*
* @param gc
* the swt GC.
* @param x
* the x coordinate of the origin of the annotation.
* @param y
* the y coordinate of the origin of the annotation.
* @param w
* the width of the annotation.
* @param h
* the height of the annotation.
* @param fill
* the annotation's fill color.
* @param border
* the annotation's border color.
* @return the annotation (a Rectangle).
*/
private Rectangle drawAnnotation(GC gc, int x, int y, int w, int h, Color fill, Color border) {
Rectangle rect = new Rectangle(x, y, w, h);
gc.setBackground(fill);
gc.fillRectangle(rect);
gc.setForeground(border);
gc.drawRectangle(x, y, w, h);
return rect;
}
/**
* Returns, for the given tree item, the deepest visible {@link TreeItem} in the Treeviewer associated
* with this TreeRuler.
*
* @param currentItem
* the given tree item.
* @param deepestVisibleItem
* the deepest visible tree item (a parent or the item itself) of the given item.
* @return the deepest visible tree item (a parent or the item itself).
*/
private TreeItem getDeepestVisibleTreeItem(final TreeItem currentItem,
final TreeItem deepestVisibleItem) {
TreeItem item = null;
if (!currentItem.isDisposed()) {
TreeItem parent = currentItem.getParentItem();
if (parent == null) {
item = deepestVisibleItem;
} else if (parent.getExpanded()) {
item = getDeepestVisibleTreeItem(parent, deepestVisibleItem);
} else {
item = getDeepestVisibleTreeItem(parent, parent);
}
}
return item;
}
/**
* Get the previous item of the given {@link TreeItem}.
*
* @param treeItem
* the given {@link TreeItem}.
* @param index
* the index of the previous item.
* @return the previous item of the given {@link TreeItem}.
*/
private TreeItem getPreviousItem(TreeItem treeItem, int index) {
TreeItem previousItem = treeItem;
if (index > 0) {
TreeItem parentItem = treeItem.getParentItem();
if (parentItem != null) {
int treeItemIndex = 0;
for (TreeItem siblingItem : parentItem.getItems()) {
if (siblingItem.equals(treeItem)) {
break;
}
treeItemIndex++;
}
if (treeItemIndex == 0) {
previousItem = getPreviousItem(parentItem, index - 1);
} else if (treeItemIndex == 1) {
TreeItem firstChild = parentItem.getItem(0);
previousItem = getLastVisibleItem(firstChild);
previousItem = getPreviousItem(previousItem, index - 1);
} else {
previousItem = getPreviousItem(getLastVisibleItem(parentItem.getItem(treeItemIndex - 1)),
index - 1);
}
} else {
// It is a root item. May be there are some previous root items.
Tree tree = treeItem.getParent();
int treeItemIndex = 0;
for (TreeItem siblingItem : tree.getItems()) {
if (siblingItem.equals(treeItem)) {
break;
}
treeItemIndex++;
}
if (treeItemIndex == 0) {
previousItem = treeItem;
} else if (treeItemIndex == 1) {
TreeItem firstRoot = tree.getItem(0);
if (firstRoot.getExpanded()) {
previousItem = getLastVisibleItem(firstRoot);
} else {
previousItem = firstRoot;
}
previousItem = getPreviousItem(previousItem, index - 1);
} else {
previousItem = tree.getItem(treeItemIndex - index);
}
}
}
return previousItem;
}
/**
* Returns the last visible {@link TreeItem} in the Treeviewer associated with this TreeRuler.
*
* @return the last visible TreeItem in the Treeviewer associated with this TreeRuler.
*/
private TreeItem getLastVisibleItem() {
int rootChildren = fTreeViewer.getTree().getItemCount();
return getLastVisibleItem(fTreeViewer.getTree().getItem(rootChildren - 1));
}
/**
* Returns the last visible child of the given {@link TreeItem} in the Treeviewer associated with this
* TreeRuler.
*
* @param item
* teh given TreeItem.
* @return the last visible child of the given TreeItem in the Treeviewer associated with this TreeRuler.
*/
private TreeItem getLastVisibleItem(TreeItem item) {
TreeItem lastVisibleItem = null;
int directChildren = item.getItemCount();
if (directChildren == 0 || !item.getExpanded()) {
lastVisibleItem = item;
} else {
TreeItem lastDirectChildren = item.getItem(directChildren - 1);
if (lastDirectChildren.getData() == null) {
lastVisibleItem = item;
} else if (lastDirectChildren.getExpanded()) {
lastVisibleItem = getLastVisibleItem(lastDirectChildren);
} else {
lastVisibleItem = lastDirectChildren;
}
}
return lastVisibleItem;
}
static class TreeNodeToVisibleTreeItem {
private final TreeNode treeNode;
private final TreeItem treeItem;
public TreeNodeToVisibleTreeItem(TreeNode treeNode, TreeItem treeItem) {
this.treeNode = treeNode;
this.treeItem = treeItem;
}
public TreeNode getTreeNode() {
return treeNode;
}
public TreeItem getTreeItem() {
return treeItem;
}
}
}