/******************************************************************************* * Copyright (c) 2012, 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.rcp.ui.internal.mergeviewer.impl; import com.google.common.base.Objects; import org.eclipse.emf.compare.Comparison; import org.eclipse.emf.compare.ConflictKind; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.DifferenceKind; import org.eclipse.emf.compare.internal.utils.ComparisonUtil; import org.eclipse.emf.compare.rcp.ui.internal.configuration.IEMFCompareConfiguration; import org.eclipse.emf.compare.rcp.ui.internal.util.MergeViewerUtil; import org.eclipse.emf.compare.rcp.ui.mergeviewer.ICompareColor; import org.eclipse.emf.compare.rcp.ui.mergeviewer.item.IMergeViewerItem; import org.eclipse.jface.viewers.IElementComparer; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Region; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Scrollable; /** * An abstract specialization of {@link AbstractStructuredMergeViewer} for Tables or Trees. * * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a> * @since 4.0 */ public abstract class AbstractTableOrTreeMergeViewer extends AbstractStructuredMergeViewer { /** IMAGE_GAP. */ static final int IMAGE_GAP = 5; /** DELTA_IMAGE_GAP. */ static final int DELTA_IMAGE_GAP = 2; /** TEXT_GAP. */ static final int TEXT_GAP = 2; /** CELL_GAP. */ static final int CELL_GAP = 1; /** The color provider of this viewer. */ private final ICompareColor.Provider fColorProvider; /** A listener that listen to erase items events. */ private Listener fEraseItemListener; /** A listener that listen to paint items events. */ private Listener fPaintItemListener; /** This will be used in order to resize the table items to an even height. */ private MesureItemListener fMesureItemListener; /** * Default constructor. * * @param parent * the parent widget of this viewer. * @param side * the side of this viewer. * @param colorProvider * the color provider to use with this viewer. * @param compareConfiguration * the compare configuration object to use with this viewer. */ public AbstractTableOrTreeMergeViewer(Composite parent, MergeViewerSide side, ICompareColor.Provider colorProvider, IEMFCompareConfiguration compareConfiguration) { super(parent, side, compareConfiguration); getStructuredViewer().setUseHashlookup(true); getStructuredViewer().setComparer(new ElementComparer()); fColorProvider = colorProvider; fEraseItemListener = new Listener() { public void handleEvent(Event event) { AbstractTableOrTreeMergeViewer.this.handleEraseItemEvent(event); } }; getStructuredViewer().getControl().addListener(SWT.EraseItem, fEraseItemListener); fPaintItemListener = new Listener() { public void handleEvent(Event event) { AbstractTableOrTreeMergeViewer.this.handlePaintItemEvent(event); } }; getStructuredViewer().getControl().addListener(SWT.PaintItem, fPaintItemListener); fMesureItemListener = new MesureItemListener(); getStructuredViewer().getControl().addListener(SWT.MeasureItem, fMesureItemListener); } /** * Handle the paint item event. * * @param event * the paint item event. */ protected void handlePaintItemEvent(Event event) { AbstractTableOrTreeItemWrapper itemWrapper = AbstractTableOrTreeItemWrapper.create((Item)event.item); String text = itemWrapper.getText(event.index); Image image = itemWrapper.getImage(event.index); /* center column 1 vertically */ Point size = event.gc.textExtent(text); int yOffset = Math.max(0, (event.height - size.y) / 2); int xOffset = event.x; if (image != null) { int imageYOffset = Math.max(0, (event.height - image.getBounds().height) / 2); event.gc.drawImage(image, event.x + IMAGE_GAP, event.y + imageYOffset); xOffset += IMAGE_GAP + image.getBounds().width; } event.gc.drawText(text, xOffset + TEXT_GAP, event.y + yOffset, true); event.width += 2; } /** * Handle the erase item event. * * @param event * the erase item event. */ protected void handleEraseItemEvent(Event event) { // let the #handlePaintItem handle the painting of foreground event.detail &= ~SWT.FOREGROUND; Item item = (Item)event.item; AbstractTableOrTreeItemWrapper itemWrapper = AbstractTableOrTreeItemWrapper.create(item); Object data = itemWrapper.getData(); if (data instanceof IMergeViewerItem) { IMergeViewerItem mergeViewerItem = ((IMergeViewerItem)data); Diff diff = mergeViewerItem.getDiff(); if (diff != null) { if (!MergeViewerUtil.isMarkAsMerged(diff, mergeViewerItem, getCompareConfiguration())) { if (mergeViewerItem.isInsertionPoint()) { paintItemDiffBox(event, itemWrapper, diff, getBoundsForInsertionPoint(event, itemWrapper)); } else { paintItemDiffBox(event, itemWrapper, diff, getBounds(event, itemWrapper)); } } } } } /** * Paint a box around the given diff, and a line that will be related to associated element in the * opposite viewer. * * @param event * the paint item event. * @param itemWrapper * a TableItemWrapper or TreeItemWrapper. * @param diff * the given Diff. * @param bounds * a Rectangle that contains coordinates of the box to paint. */ private void paintItemDiffBox(Event event, AbstractTableOrTreeItemWrapper itemWrapper, Diff diff, Rectangle bounds) { event.detail &= ~SWT.HOT; GC g = event.gc; Color oldBackground = g.getBackground(); Color oldForeground = g.getForeground(); setGCStyleForDiff(g, diff, isSelected(event)); g.fillRectangle(bounds); g.drawRectangle(bounds); if (diff.getKind() == DifferenceKind.MOVE) { g.setLineStyle(SWT.LINE_SOLID); } switch (getSide()) { case LEFT: drawLineFromBoxToCenter(itemWrapper, bounds, g); break; case RIGHT: drawLineFromCenterToBox(itemWrapper, bounds, g); break; default: break; } if (isSelected(event)) { g.setForeground(event.display.getSystemColor(SWT.COLOR_LIST_FOREGROUND)); g.setBackground(event.display.getSystemColor(SWT.COLOR_LIST_BACKGROUND)); event.detail &= ~SWT.SELECTED; } else { g.setBackground(oldBackground); g.setForeground(oldForeground); } } /** * Draw a line from center to box. * * @param itemWrapper * a TableItemWrapper or TreeItemWrapper. * @param boxBounds * a Rectangle that contains coordinates of the box. * @param g * the SWT GC tool. */ private void drawLineFromCenterToBox(AbstractTableOrTreeItemWrapper itemWrapper, Rectangle boxBounds, GC g) { AbstractTableOrTreeItemWrapper parent = itemWrapper.getParentItem(); final int xOffset; if (getContentProvider() instanceof ITreeContentProvider) { final boolean hasChildren = ((ITreeContentProvider)getContentProvider()) .hasChildren(itemWrapper.getData()); if (hasChildren) { if (parent != null) { xOffset = parent.getImageBounds(0).x; } else { xOffset = 0; } } else { xOffset = boxBounds.x; } } else { xOffset = boxBounds.x; } Rectangle itemBounds = itemWrapper.getBounds(); Point from = new Point(0, 0); from.y = itemBounds.y + (itemBounds.height / 2); Point to = new Point(xOffset, from.y); g.drawLine(from.x, from.y, to.x, to.y); } /** * Draw a line from box to center. * * @param itemWrapper * a TableItemWrapper or TreeItemWrapper. * @param boxBounds * a Rectangle that contains coordinates of the box. * @param g * the SWT GC tool. */ private void drawLineFromBoxToCenter(AbstractTableOrTreeItemWrapper itemWrapper, Rectangle boxBounds, GC g) { Rectangle itemBounds = itemWrapper.getBounds(); Rectangle clientArea = itemWrapper.getParent().getClientArea(); Point from = new Point(0, 0); from.x = boxBounds.x + boxBounds.width; from.y = itemBounds.y + (itemBounds.height / 2); Point to = new Point(0, from.y); to.x = clientArea.x + clientArea.width; g.drawLine(from.x, from.y, to.x, to.y); } /** * Computes the bounds (as Rectangle) in case of the given Item is an insertion point. * * @param event * the paint item event. * @param itemWrapper * a TableItemWrapper or TreeItemWrapper. * @return the bounds (as Rectangle) of the insertion point. */ private static Rectangle getBoundsForInsertionPoint(Event event, AbstractTableOrTreeItemWrapper itemWrapper) { Rectangle fill = getBounds(event, itemWrapper); Rectangle treeBounds = itemWrapper.getParent().getClientArea(); Rectangle itemBounds = itemWrapper.getBounds(); fill.x = itemWrapper.getImageBounds(0).x + 2; fill.y = fill.y + (itemBounds.height / 3); fill.width = treeBounds.width / 4; fill.height = itemBounds.height / 3; return fill; } /** * Set the background, foreground colors and the line style for the given diff. * * @param g * the SWT GC tool. * @param diff * the given Diff. * @param selected * is the Diff selected or not. */ private void setGCStyleForDiff(GC g, Diff diff, boolean selected) { final Comparison comparison = ComparisonUtil.getComparison(diff); final boolean isThreeWay = comparison.isThreeWay(); if (diff.getKind() == DifferenceKind.MOVE) { g.setLineStyle(SWT.LINE_DOT); } g.setForeground(fColorProvider.getCompareColor().getStrokeColor(diff, isThreeWay, false, selected)); g.setBackground(fColorProvider.getCompareColor().getFillColor(diff, isThreeWay, false, selected)); } /** * Computes the bounds (as Rectangle) of the given Item. * * @param event * the paint item event. * @param itemWrapper * a TableItemWrapper or TreeItemWrapper. * @return the bounds (as Rectangle) of the given Item. */ private static Rectangle getBounds(Event event, AbstractTableOrTreeItemWrapper itemWrapper) { Scrollable tree = itemWrapper.getParent(); Rectangle treeBounds = tree.getClientArea(); Rectangle itemBounds = itemWrapper.getBounds(); Rectangle imageBounds = itemWrapper.getImageBounds(0); Rectangle fill = new Rectangle(0, 0, 0, 0); fill.x = itemBounds.x - imageBounds.width; fill.y = itemBounds.y; if (!"cocoa".equals(SWT.getPlatform())) { //$NON-NLS-1$ fill.y += 1; } // +x to add the icon and the expand "+" if we are in a tree fill.width = itemBounds.width + imageBounds.width + DELTA_IMAGE_GAP; fill.height = itemBounds.height - 1; if (!"cocoa".equals(SWT.getPlatform())) { //$NON-NLS-1$ fill.height -= 3; } final GC g = event.gc; // If you wish to paint the selection beyond the end of last column, you must change the clipping // region. int columnCount = itemWrapper.getParentColumnCount(); if (event.index == columnCount - 1 || columnCount == 0) { int width = treeBounds.x + treeBounds.width - event.x; if (width > 0) { Region region = new Region(); g.getClipping(region); region.add(event.x, event.y, width, event.height); g.setClipping(region); region.dispose(); } } g.setAdvanced(true); return fill; } /** * Check the event indicates a user-interface component state is selected. * * @param event * the event. * @return true, if the event indicates a user-interface component state is selected, false otherwise. */ private static boolean isSelected(Event event) { return (event.detail & SWT.SELECTED) != 0; } /** * {@inheritDoc} * * @see org.eclipse.emf.compare.rcp.ui.internal.mergeviewer.impl.AbstractStructuredMergeViewer#handleDispose(org.eclipse.swt.events.DisposeEvent) */ @Override protected void handleDispose(DisposeEvent event) { getStructuredViewer().getControl().removeListener(SWT.MeasureItem, fMesureItemListener); getStructuredViewer().getControl().removeListener(SWT.EraseItem, fEraseItemListener); getStructuredViewer().getControl().removeListener(SWT.PaintItem, fPaintItemListener); super.handleDispose(event); } /** * A specific implementation of {@link IElementComparer} that compare EMF Compare Viewer Items. * * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a> * @since 4.0 */ public static final class ElementComparer implements IElementComparer { /** * {@inheritDoc}. */ public int hashCode(Object element) { final int hashCode; if (element instanceof IMergeViewerItem) { IMergeViewerItem item = (IMergeViewerItem)element; Diff diff = item.getDiff(); if (diff != null && diff.getConflict() != null && diff.getConflict().getKind() == ConflictKind.PSEUDO) { // we do not create only one item per diff in pseudo conflict, so we hash the conflict and // not the diff hashCode = Objects.hashCode(item.getAncestor(), diff.getConflict()); } else if (diff != null && item.getLeft() == null && item.getRight() == null) { hashCode = Objects.hashCode(item.getAncestor(), diff); } else { hashCode = Objects.hashCode(item.getLeft(), item.getRight(), item.getAncestor(), diff); } } else { hashCode = element.hashCode(); } return hashCode; } /** * {@inheritDoc}. */ public boolean equals(Object a, Object b) { final boolean ret; if (a != b && a instanceof IMergeViewerItem && b instanceof IMergeViewerItem) { IMergeViewerItem itemA = (IMergeViewerItem)a; IMergeViewerItem itemB = (IMergeViewerItem)b; Diff diffA = itemA.getDiff(); Diff diffB = itemB.getDiff(); if (diffA != null && diffA.getConflict() != null && diffA.getConflict().getKind() == ConflictKind.PSEUDO && diffB != null && diffB.getConflict() != null && diffB.getConflict().getKind() == ConflictKind.PSEUDO) { // pseudo conflict ret = Objects.equal(itemA.getAncestor(), itemB.getAncestor()) && Objects.equal(diffA.getConflict(), diffB.getConflict()); } else if (diffA != null && diffB != null && itemA.getLeft() == null && itemA.getRight() == null && itemB.getLeft() == null && itemB.getRight() == null) { ret = Objects.equal(diffA, diffB); } else { ret = Objects.equal(itemA.getLeft(), itemB.getLeft()) && Objects.equal(itemA.getRight(), itemB.getRight()) && Objects.equal(itemA.getAncestor(), itemB.getAncestor()) && Objects.equal(diffA, diffB); } } else { ret = Objects.equal(a, b); } return ret; } } /** * This will be used in order to resize the table items to an even height. Otherwise the lines we draw * around it look somewhat flattened. * * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a> * @since 4.0 */ public static class MesureItemListener implements Listener { /** * The height to which we will resize items. */ private int fHeight = Integer.MIN_VALUE; /** * {@inheritDoc} * * @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event) */ public void handleEvent(Event event) { if (fHeight == Integer.MIN_VALUE) { AbstractTableOrTreeItemWrapper itemWrapper = AbstractTableOrTreeItemWrapper .create((Item)event.item); Rectangle imageBounds = itemWrapper.getImageBounds(0); fHeight = imageBounds.height + 3; } event.height = fHeight; } } }