/*******************************************************************************
* Copyright (c) 2011 EBM WebSourcing (PetalsLink)
* 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:
* Mickael Istria, EBM WebSourcing (PetalsLink) - initial API and implementation
*******************************************************************************/
package org.eclipse.nebula.widgets.treemapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.ImageFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.LightweightSystem;
import org.eclipse.draw2d.MouseEvent;
import org.eclipse.draw2d.MouseListener;
import org.eclipse.draw2d.MouseMotionListener;
import org.eclipse.draw2d.XYLayout;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.nebula.widgets.treemapper.internal.LinkFigure;
import org.eclipse.nebula.widgets.treemapper.internal.Messages;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TreeDragSourceEffect;
import org.eclipse.swt.dnd.TreeDropTargetEffect;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.TreeItem;
/**
* A TreeMapper is a composite viewer the creates 2 {@link TreeViewer} (left and right)
* and an area to display mappings between tree nodes.
* It relies on a {@link ISemanticTreeMapperSupport} to create your business mapping objects,
* and to resolve the bounds of a mapping object to object that are provided in the trees.
*
* @author Mickael Istria (EBM WebSourcing (PetalsLink))
* @since 0.1.0
* @noextend This class is not intended to be subclassed by clients.
*
* @param <M> The type of the business <b>M<b>apping object
* @param <L> The type of the left bound of the mapping (as provided by <b>L</b>eft {@link ITreeContentProvider})
* @param <R> The type of the left bound of the mapping (as provided by <b>R</>ight {@link ITreeContentProvider})
*/
public class TreeMapper<M, L, R> implements ISelectionProvider {
private SashForm control;
private TreeMapperUIConfigProvider uiConfig;
private TreeViewer leftTreeViewer;
private TreeViewer rightTreeViewer;
private TreeItem leftTopItem;
private TreeItem rightTopItem;
private Canvas linkCanvas;
private LightweightSystem linkSystem;
private Figure linkRootFigure;
private boolean canvasNeedRedraw;
private List<M> mappings;
private Map<LinkFigure, M> figuresToMappings;
private Map<M, LinkFigure> mappingsToFigures;
private LinkFigure selectedFigure;
private M selectedMapping;
private ISemanticTreeMapperSupport<M, L, R> semanticSupport;
private IFigure warningFigure;
public TreeMapper(Composite parent, ISemanticTreeMapperSupport<M, L, R> semanticSupport, TreeMapperUIConfigProvider uiConfig) {
this.uiConfig = uiConfig;
this.semanticSupport = semanticSupport;
control = new SashForm(parent, SWT.HORIZONTAL);
control.setLayout(new FillLayout());
// left
leftTreeViewer = new TreeViewer(control);
//center
linkCanvas = new Canvas(control, SWT.NONE);
linkCanvas.setLayout(new FillLayout());
linkCanvas.setBackground(ColorConstants.white);
linkSystem = new LightweightSystem(linkCanvas);
linkRootFigure = new Figure();
linkRootFigure.setLayoutManager(new XYLayout());
linkSystem.setContents(linkRootFigure);
// right
rightTreeViewer = new TreeViewer(control);
figuresToMappings = new HashMap<LinkFigure, M>();
mappingsToFigures = new HashMap<M, LinkFigure>();
// Resize
ControlListener resizeListener = new ControlListener() {
public void controlResized(ControlEvent e) {
canvasNeedRedraw = true;
}
public void controlMoved(ControlEvent e) {
canvasNeedRedraw = true;
}
};
leftTreeViewer.getTree().addControlListener(resizeListener);
rightTreeViewer.getTree().addControlListener(resizeListener);
linkCanvas.addControlListener(resizeListener);
// Scroll
leftTreeViewer.getTree().addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
if (canvasNeedRedraw || leftTreeViewer.getTree().getTopItem() != leftTopItem) {
leftTopItem = leftTreeViewer.getTree().getTopItem();
redrawMappings();
}
}
});
rightTreeViewer.getTree().addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
if (canvasNeedRedraw || rightTreeViewer.getTree().getTopItem() != rightTopItem) {
rightTopItem = rightTreeViewer.getTree().getTopItem();
redrawMappings();
canvasNeedRedraw = false;
}
}
});
// Expand
TreeListener treeListener = new TreeListener() {
public void treeExpanded(TreeEvent e) {
canvasNeedRedraw = true;
}
public void treeCollapsed(TreeEvent e) {
canvasNeedRedraw = true;
}
};
leftTreeViewer.getTree().addTreeListener(treeListener);
rightTreeViewer.getTree().addTreeListener(treeListener);
control.setWeights(new int[] { 1, 2, 1} );
bindTreeForDND(leftTreeViewer, rightTreeViewer, SWT.LEFT_TO_RIGHT);
bindTreeForDND(rightTreeViewer, leftTreeViewer, SWT.RIGHT_TO_LEFT);
}
/**
* Set the content providers for both trees.
* Both tree provides MUST HAVE their {@link ITreeContentProvider#getParent(Object)} method implemeneted.
* @param leftContentProvider An {@link ITreeContentProvider} that node are instances of the <b>L<b> type parameter.
* @param rightTreeContentProvider An {@link ITreeContentProvider} that node are instances of the <b>R<b> type parameter.
*/
public void setContentProviders(ITreeContentProvider leftContentProvider, ITreeContentProvider rightTreeContentProvider) {
leftTreeViewer.setContentProvider(leftContentProvider);
rightTreeViewer.setContentProvider(rightTreeContentProvider);
}
public void setLabelProviders(IBaseLabelProvider leftLabelProvider, IBaseLabelProvider rightLabelProvider) {
leftTreeViewer.setLabelProvider(leftLabelProvider);
rightTreeViewer.setLabelProvider(rightLabelProvider);
}
/**
* Sets the input of the widget.
* @param leftTreeInput The input for left {@link TreeViewer}
* @param rightTreeInput The input for right {@link TreeViewer}
* @param mappings The list containing the mapping. It will be used as a working copy and
* then MODIFIED by the tree mapper. If you don't want to pass a modifiable list, then pass
* a copy of the default mapping list, and prefer using {@link TreeMapper}{@link #addNewMappingListener(INewMappingListener)}
* and {@link INewMappingListener} to track the creation of mapping.
*/
public void setInput(Object leftTreeInput, Object rightTreeInput, List<M> mappings) {
clearFigures();
if (leftTreeInput != null) {
leftTreeViewer.setInput(leftTreeInput);
}
if (rightTreeInput != null) {
rightTreeViewer.setInput(rightTreeInput);
}
if (mappings != null) {
this.mappings = mappings;
canvasNeedRedraw = true;
} else {
this.mappings = new ArrayList<M>();
}
// Synchronize tree and viewers for mappings:
// Expand left and right items of mappings, and then restore
// tree to previous state
for (M mapping : this.mappings) {
L leftItem = semanticSupport.resolveLeftItem(mapping);
leftTreeViewer.expandToLevel(leftItem, 0);
R rightItem = semanticSupport.resolveRightItem(mapping);
rightTreeViewer.expandToLevel(rightItem, 0);
}
}
/**
* DO NOT USE IN CODE. Prefer setting "canvasNeedsRedraw" field to true to
* avoid useless operations.
* @param mappings
*/
private void redrawMappings() {
if (this.mappings == null) {
return;
}
boolean everythingOK = true;
for (M mapping : this.mappings) {
everythingOK &= drawMapping(mapping);
if (mapping == selectedMapping) {
LinkFigure newSelectedFigure = mappingsToFigures.get(mapping);
applySelectedMappingFeedback(newSelectedFigure);
selectedFigure = newSelectedFigure;
}
}
if (everythingOK && warningFigure != null) {
linkRootFigure.remove(warningFigure);
warningFigure = null;
} else if (!everythingOK && warningFigure == null) {
warningFigure = createWarningFigure();
linkRootFigure.add(warningFigure, new Rectangle(5, 5, SWT.DEFAULT, SWT.DEFAULT));
}
}
/**
* @return a newly created figure to alert the end-user of an inconsistency in the widget
*/
private IFigure createWarningFigure() {
Image image = Display.getDefault().getSystemImage(SWT.ICON_WARNING);
ImageFigure res = new ImageFigure(image);
res.setPreferredSize(10, 10);
Label label = new Label(Messages.widgetInconsistency);
res.setToolTip(label);
return res;
}
/**
* @param sourceTreeViewer
* @param targetTreeViewer
* @param direction
*/
private void bindTreeForDND(final TreeViewer sourceTreeViewer, final TreeViewer targetTreeViewer, final int direction) {
final LocalSelectionTransfer sourceTransfer = LocalSelectionTransfer.getTransfer();
final LocalSelectionTransfer targetTransfer = LocalSelectionTransfer.getTransfer();
sourceTreeViewer.addDragSupport(DND.DROP_LINK, new Transfer[] { sourceTransfer }, new TreeDragSourceEffect(sourceTreeViewer.getTree()) {
@Override
public void dragStart(DragSourceEvent event) {
event.doit = !sourceTreeViewer.getSelection().isEmpty();
}
});
targetTreeViewer.addDropSupport(DND.DROP_LINK, new Transfer[] { targetTransfer }, new TreeDropTargetEffect(targetTreeViewer.getTree()) {
@Override
public void dragEnter(DropTargetEvent event) {
event.feedback = DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL | DND.FEEDBACK_SELECT;
event.detail = DND.DROP_LINK;
super.dragEnter(event);
}
@Override
public void drop(DropTargetEvent event) {
performMappingByDrop(sourceTreeViewer, sourceTreeViewer.getSelection(), targetTreeViewer, (TreeItem) getItem(event.x, event.y), direction);
}
});
}
/**
* @param targetTreeViewer
* @param data
* @param widget
*/
protected void performMappingByDrop(TreeViewer sourceTreeViewer, ISelection sourceData, TreeViewer targetTreeViewer, TreeItem targetTreeItem, int direction) {
Object resolvedTargetItem = resolveTreeViewerItem(targetTreeViewer, targetTreeItem);
for (Object sourceItem : ((IStructuredSelection)sourceData).toList()) {
if (direction == SWT.LEFT_TO_RIGHT) {
createMapping((L)sourceItem, (R)resolvedTargetItem);
} else if (direction == SWT.RIGHT_TO_LEFT) {
createMapping((L)resolvedTargetItem, (R)sourceItem);
}
}
}
/**
* @param leftItem
* @param resolvedTargetItem
*/
private void createMapping(L leftItem, R rightItem) {
M newMapping = semanticSupport.createSemanticMappingObject(leftItem, rightItem);
if (newMapping != null) {
mappings.add(newMapping);
refresh();
drawMapping(newMapping);
for (INewMappingListener<M> listener : creationListeners) {
listener.mappingCreated(newMapping);
}
}
}
/**
* Draw a mapping and returns whether the operation is successful or not.
* If not, a message is logged to help in debugging.
* @param leftItem
* @param rightItem
* @return true is successful, false if an issue occured
*/
private boolean drawMapping(final M mapping) {
LinkFigure previousFigure = mappingsToFigures.get(mapping);
if (previousFigure != null) {
previousFigure.deleteFromParent();
mappingsToFigures.remove(mapping);
figuresToMappings.remove(previousFigure);
}
final LinkFigure arrowFigure = new LinkFigure(linkRootFigure);
{
boolean leftItemVisible = true;
TreeItem leftTreeItem = (TreeItem) leftTreeViewer.testFindItem(semanticSupport.resolveLeftItem(mapping));
if (leftTreeItem == null) {
Policy.getLog().log(
new Status(IStatus.ERROR,
"org.eclipse.nebula.widgets.treemapper",
"Could not find left entry of mapping " + mapping.toString() + " in left treeViewer."));
return false;
}
TreeItem lastVisibleLeftTreeItem = leftTreeItem;
while (leftTreeItem.getParentItem() != null) {
if (!leftTreeItem.getParentItem().getExpanded()) {
lastVisibleLeftTreeItem = leftTreeItem.getParentItem();
leftItemVisible = false;
}
leftTreeItem = leftTreeItem.getParentItem();
}
arrowFigure.setLeftPoint(0, lastVisibleLeftTreeItem.getBounds().y + lastVisibleLeftTreeItem.getBounds().height / 2);
arrowFigure.setLeftMappingVisible(leftItemVisible);
}
{
boolean rightItemVisible = true;
TreeItem rightTreeItem = (TreeItem) rightTreeViewer.testFindItem(semanticSupport.resolveRightItem(mapping));
if (rightTreeItem == null) {
Policy.getLog().log(
new Status(IStatus.ERROR,
"org.eclipse.nebula.widgets.treemapper",
"Could not find right entry of mapping " + mapping.toString() + " in right treeViewer."));
return false;
}
TreeItem lastVisibleRightTreeItem = rightTreeItem;
while (rightTreeItem.getParentItem() != null) {
if (!rightTreeItem.getParentItem().getExpanded()) {
lastVisibleRightTreeItem = rightTreeItem.getParentItem();
rightItemVisible = false;
}
rightTreeItem = rightTreeItem.getParentItem();
}
arrowFigure.setRightPoint(linkRootFigure.getBounds().width, lastVisibleRightTreeItem.getBounds().y + rightTreeItem.getBounds().height / 2);
arrowFigure.setRightMappingVisible(rightItemVisible);
}
arrowFigure.setLineWidth(uiConfig.getDefaultArrowWidth());
arrowFigure.seLineColor(uiConfig.getDefaultMappingColor());
arrowFigure.addMouseListener(new MouseListener() {
public void mousePressed(MouseEvent me) {
fireMappingSelection(mapping, arrowFigure);
}
public void mouseReleased(MouseEvent me) {
}
public void mouseDoubleClicked(MouseEvent me) {
//if (arrowFigure.)
}
});
arrowFigure.addMouseMotionListener(new MouseMotionListener() {
public void mouseDragged(MouseEvent me) {
}
public void mouseEntered(MouseEvent me) {
fireMouseEntered(mapping, arrowFigure);
}
public void mouseExited(MouseEvent me) {
fireMouseExited(mapping, arrowFigure);
}
public void mouseHover(MouseEvent me) {
}
public void mouseMoved(MouseEvent me) {
}
});
// store it
figuresToMappings.put(arrowFigure, mapping);
mappingsToFigures.put(mapping, arrowFigure);
return true;
}
/**
* @param treeViewer
* @param treeItem
* @return
*/
private Object resolveTreeViewerItem(TreeViewer treeViewer, TreeItem treeItem) {
//return treeItem.getData();
ITreeContentProvider contentProvider = (ITreeContentProvider) treeViewer.getContentProvider();
List<Integer> locations = new ArrayList<Integer>();
TreeItem parentTreeItem = treeItem.getParentItem();
while (parentTreeItem != null) {
int index = Arrays.asList(parentTreeItem.getItems()).indexOf(treeItem);
locations.add(index);
treeItem = parentTreeItem;
parentTreeItem = treeItem.getParentItem();
}
// root
if (treeItem != null) {
int rootIndex = Arrays.asList(treeViewer.getTree().getItems()).indexOf(treeItem);
locations.add(rootIndex);
}
Collections.reverse(locations);
Object current = contentProvider.getElements(treeViewer.getInput())[locations.get(0)];
locations.remove(0);
for (int index : locations) {
current = contentProvider.getChildren(current)[index];
}
return current;
}
/**
* @return
*/
public SashForm getControl() {
return control;
}
//
// Selection management
//
private List<ISelectionChangedListener> selectionChangedListeners = new ArrayList<ISelectionChangedListener>();
private IStructuredSelection currentSelection = new StructuredSelection();
public void addSelectionChangedListener(ISelectionChangedListener listener) {
this.selectionChangedListeners.add(listener);
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#getSelection()
*/
public IStructuredSelection getSelection() {
return currentSelection;
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#removeSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
*/
public void removeSelectionChangedListener(ISelectionChangedListener listener) {
selectionChangedListeners.remove(listener);
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#setSelection(org.eclipse.jface.viewers.ISelection)
*/
public void setSelection(ISelection selection) {
IStructuredSelection strSelection = (IStructuredSelection)selection;
if (strSelection.isEmpty()) {
currentSelection = new StructuredSelection();
fireMouseExited(selectedMapping, mappingsToFigures.get(selectedMapping));
} else {
M mapping = (M) strSelection.getFirstElement();
fireMappingSelection(mapping, mappingsToFigures.get(mapping));
}
}
/**
* @param mapping
* @param arrowFigure
*/
protected void fireMappingSelection(M mapping, LinkFigure arrowFigure) {
if (selectedFigure != null) {
applyDefaultMappingStyle(selectedFigure);
}
applySelectedMappingFeedback(arrowFigure);
selectedFigure = arrowFigure;
selectedMapping = mapping;
currentSelection = new StructuredSelection(selectedMapping);
for (ISelectionChangedListener listener : selectionChangedListeners) {
listener.selectionChanged(new SelectionChangedEvent(this, currentSelection));
}
}
/**
* Select no item
*/
private void unselect() {
selectedMapping = null;
selectedFigure = null;
currentSelection = new StructuredSelection();
for (ISelectionChangedListener listener : selectionChangedListeners) {
listener.selectionChanged(new SelectionChangedEvent(this, currentSelection));
}
}
//
// Creation management
//
private List<INewMappingListener<M>> creationListeners = new ArrayList<INewMappingListener<M>>();
/**
* @param iNewMappingListener
*/
public void addNewMappingListener(INewMappingListener<M> listener) {
this.creationListeners.add(listener);
}
/**
*
*/
private void applyDefaultMappingStyle(LinkFigure figure) {
figure.seLineColor(uiConfig.getDefaultMappingColor());
figure.setLineWidth(uiConfig.getDefaultArrowWidth());
}
/**
* @param arrowFigure
*/
private void applySelectedMappingFeedback(LinkFigure arrowFigure) {
arrowFigure.seLineColor(uiConfig.getSelectedMappingColor());
arrowFigure.setLineWidth(uiConfig.getHoverArrowWidth());
}
/**
* @param mapping
* @param arrowFigure
*/
protected void fireMouseExited(M mapping, LinkFigure arrowFigure) {
if (arrowFigure != selectedFigure) {
applyDefaultMappingStyle(arrowFigure);
}
}
/**
* @param mapping
* @param arrowFigure
*/
protected void fireMouseEntered(M mapping, LinkFigure arrowFigure) {
if (arrowFigure != selectedFigure) {
arrowFigure.setLineWidth(uiConfig.getHoverArrowWidth());
}
}
/**
* @return
*/
public TreeViewer getLeftTreeViewer() {
return leftTreeViewer;
}
/**
* @return
*/
public TreeViewer getRightTreeViewer() {
return rightTreeViewer;
}
/**
* Refresh the widget by resetting the setInput value
*/
public void refresh() {
setInput(leftTreeViewer.getInput(), rightTreeViewer.getInput(), mappings);
if (!mappings.contains(selectedMapping)) {
unselect();
}
leftTreeViewer.refresh();
rightTreeViewer.refresh();
canvasNeedRedraw = true;
control.layout(true);
}
/**
*
*/
private void clearFigures() {
for (Entry<M, LinkFigure> entry : mappingsToFigures.entrySet()) {
entry.getValue().deleteFromParent();
}
mappingsToFigures.clear();
figuresToMappings.clear();
}
}