blob: aed63ed1011be328819f9e1f4daadc2e006f7f26 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2009 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.compare.structuremergeviewer;
import java.util.Iterator;
import java.util.ResourceBundle;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.CompareViewerPane;
import org.eclipse.compare.INavigatable;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.compare.internal.patch.DiffViewerComparator;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
/**
* A tree viewer that works on objects implementing
* the {@code IDiffContainer} and {@code IDiffElement} interfaces.
* <p>
* This class may be instantiated; it is not intended to be subclassed outside
* of this package.
* </p>
*
* @see IDiffContainer
* @see IDiffElement
* @noextend This class is not intended to be subclassed by clients.
*/
public class DiffTreeViewer extends TreeViewer {
class DiffViewerContentProvider implements ITreeContentProvider {
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// Empty implementation.
}
public boolean isDeleted(Object element) {
return false;
}
@Override
public void dispose() {
inputChanged(DiffTreeViewer.this, getInput(), null);
}
@Override
public Object getParent(Object element) {
if (element instanceof IDiffElement)
return ((IDiffElement) element).getParent();
return null;
}
@Override
public final boolean hasChildren(Object element) {
if (element instanceof IDiffContainer)
return ((IDiffContainer) element).hasChildren();
return false;
}
@Override
public final Object[] getChildren(Object element) {
if (element instanceof IDiffContainer)
return ((IDiffContainer) element).getChildren();
return new Object[0];
}
@Override
public Object[] getElements(Object element) {
return getChildren(element);
}
}
/*
* Takes care of swapping left and right if fLeftIsLocal is true.
*/
class DiffViewerLabelProvider extends LabelProvider {
@Override
public String getText(Object element) {
if (element instanceof IDiffElement)
return ((IDiffElement) element).getName();
return Utilities.getString(fBundle, "defaultLabel"); //$NON-NLS-1$
}
@Override
@SuppressWarnings("incomplete-switch")
public Image getImage(Object element) {
if (element instanceof IDiffElement) {
IDiffElement input= (IDiffElement) element;
int kind= input.getKind();
// Flip the direction and the change type, because all images
// are the other way round, i.e. for comparison from left to right.
switch (kind & Differencer.DIRECTION_MASK) {
case Differencer.LEFT:
kind= (kind &~ Differencer.LEFT) | Differencer.RIGHT;
break;
case Differencer.RIGHT:
kind= (kind &~ Differencer.RIGHT) | Differencer.LEFT;
break;
case 0:
switch (kind & Differencer.CHANGE_TYPE_MASK) {
case Differencer.ADDITION:
kind= (kind &~ Differencer.ADDITION) | Differencer.DELETION;
break;
case Differencer.DELETION:
kind= (kind &~ Differencer.DELETION) | Differencer.ADDITION;
break;
}
}
return fCompareConfiguration.getImage(input.getImage(), kind);
}
return null;
}
/**
* Informs the platform, that the images have changed.
*/
public void fireLabelProviderChanged() {
fireLabelProviderChanged(new LabelProviderChangedEvent(this));
}
}
static class FilterSame extends ViewerFilter {
@Override
public boolean select(Viewer viewer, Object parentElement, Object element) {
if (element instanceof IDiffElement)
return (((IDiffElement) element).getKind() & Differencer.PSEUDO_CONFLICT) == 0;
return true;
}
public boolean isFilterProperty(Object element, Object property) {
return false;
}
}
private ResourceBundle fBundle;
private CompareConfiguration fCompareConfiguration;
private IPropertyChangeListener fPropertyChangeListener;
private DiffViewerLabelProvider diffViewerLabelProvider = new DiffViewerLabelProvider();
private Action fEmptyMenuAction;
private Action fExpandAllAction;
/**
* Creates a new viewer for the given SWT tree control with the specified configuration.
*
* @param tree the tree control
* @param configuration the configuration for this viewer
*/
public DiffTreeViewer(Tree tree, CompareConfiguration configuration) {
super(tree);
initialize(configuration == null ? new CompareConfiguration() : configuration);
}
/**
* Creates a new viewer under the given SWT parent and with the specified configuration.
*
* @param parent the SWT control under which to create the viewer
* @param configuration the configuration for this viewer
*/
public DiffTreeViewer(Composite parent, CompareConfiguration configuration) {
super(new Tree(parent, SWT.MULTI));
initialize(configuration == null ? new CompareConfiguration() : configuration);
}
private void initialize(CompareConfiguration configuration) {
Control tree= getControl();
INavigatable nav= new INavigatable() {
@Override
public boolean selectChange(int flag) {
if (flag == INavigatable.FIRST_CHANGE) {
setSelection(StructuredSelection.EMPTY);
flag = INavigatable.NEXT_CHANGE;
} else if (flag == INavigatable.LAST_CHANGE) {
setSelection(StructuredSelection.EMPTY);
flag = INavigatable.PREVIOUS_CHANGE;
}
// Fix for http://dev.eclipse.org/bugs/show_bug.cgi?id=20106
return internalNavigate(flag == INavigatable.NEXT_CHANGE, true);
}
@Override
public Object getInput() {
return DiffTreeViewer.this.getInput();
}
@Override
public boolean openSelectedChange() {
return internalOpen();
}
@Override
public boolean hasChange(int changeFlag) {
return getNextItem(changeFlag == INavigatable.NEXT_CHANGE, false) != null;
}
};
tree.setData(INavigatable.NAVIGATOR_PROPERTY, nav);
tree.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle());
Composite parent= tree.getParent();
fBundle= ResourceBundle.getBundle("org.eclipse.compare.structuremergeviewer.DiffTreeViewerResources"); //$NON-NLS-1$
// Register for notification with the CompareConfiguration.
fCompareConfiguration= configuration;
if (fCompareConfiguration != null) {
fPropertyChangeListener = event -> propertyChange(event);
fCompareConfiguration.addPropertyChangeListener(fPropertyChangeListener);
}
setContentProvider(new DiffViewerContentProvider());
setLabelProvider(diffViewerLabelProvider);
addSelectionChangedListener(event -> updateActions());
setComparator(new DiffViewerComparator());
ToolBarManager tbm= CompareViewerPane.getToolBarManager(parent);
if (tbm != null) {
tbm.removeAll();
tbm.add(new Separator("merge")); //$NON-NLS-1$
tbm.add(new Separator("modes")); //$NON-NLS-1$
tbm.add(new Separator("navigation")); //$NON-NLS-1$
createToolItems(tbm);
updateActions();
tbm.update(true);
}
MenuManager mm= new MenuManager();
mm.setRemoveAllWhenShown(true);
mm.addMenuListener(
mm2 -> {
fillContextMenu(mm2);
if (mm2.isEmpty()) {
if (fEmptyMenuAction == null) {
fEmptyMenuAction = new Action(Utilities.getString(fBundle, "emptyMenuItem")) { //$NON-NLS-1$
// left empty
};
fEmptyMenuAction.setEnabled(false);
}
mm2.add(fEmptyMenuAction);
}
}
);
tree.setMenu(mm.createContextMenu(tree));
}
/**
* Returns the viewer's name.
*
* @return the viewer's name
*/
public String getTitle() {
String title= Utilities.getString(fBundle, "title", null); //$NON-NLS-1$
if (title == null)
title= Utilities.getString("DiffTreeViewer.title"); //$NON-NLS-1$
return title;
}
/**
* Returns the resource bundle.
*
* @return the viewer's resource bundle
*/
protected ResourceBundle getBundle() {
return fBundle;
}
/**
* Returns the compare configuration of this viewer.
*
* @return the compare configuration of this viewer
*/
public CompareConfiguration getCompareConfiguration() {
return fCompareConfiguration;
}
/**
* Called on the viewer disposal.
* Unregisters from the compare configuration.
* Clients may extend if they have to do additional cleanup.
*
* @param event dispose event that triggered call to this method
*/
@Override
protected void handleDispose(DisposeEvent event) {
if (fCompareConfiguration != null) {
if (fPropertyChangeListener != null)
fCompareConfiguration.removePropertyChangeListener(fPropertyChangeListener);
fCompareConfiguration= null;
}
fPropertyChangeListener= null;
super.handleDispose(event);
}
/**
* Tracks property changes of the configuration object.
* Clients may extend to track their own property changes.
* In this case they must call the inherited method.
*
* @param event property change event that triggered call to this method
*/
protected void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(CompareConfiguration.MIRRORED)) {
diffViewerLabelProvider.fireLabelProviderChanged();
}
}
@Override
protected void inputChanged(Object in, Object oldInput) {
super.inputChanged(in, oldInput);
if (in != oldInput) {
initialSelection();
updateActions();
}
}
/**
* This hook method is called from within {@code inputChanged}
* after a new input has been set but before any controls are updated.
* This default implementation calls {@code navigate(true)}
* to select and expand the first leaf node.
* Clients can override this method and are free to decide whether
* they want to call the inherited method.
*
* @since 2.0
*/
protected void initialSelection() {
navigate(true);
}
/**
* Overridden to avoid expanding {@code DiffNode}s that shouldn't expand.
*
* @param node the node to expand
* @param level non-negative level, or {@code ALL_LEVELS} to collapse all levels of the tree
*/
@Override
protected void internalExpandToLevel(Widget node, int level) {
Object data= node.getData();
if (dontExpand(data))
return;
super.internalExpandToLevel(node, level);
}
/**
* This hook method is called from within {@code internalExpandToLevel}
* to control whether a given model node should be expanded or not.
* This default implementation checks whether the object is a {@code DiffNode} and
* calls {@code dontExpand()} on it.
* Clients can override this method and are free to decide whether
* they want to call the inherited method.
*
* @param o the model object to be expanded
* @return {@code false} if a node should be expanded, {@code true} to prevent expanding
* @since 2.0
*/
protected boolean dontExpand(Object o) {
return o instanceof DiffNode && ((DiffNode) o).dontExpand();
}
//---- merge action support
/**
* This factory method is called after the viewer's controls have been created.
* It installs four actions in the given {@code ToolBarManager}. Two actions
* allow for copying one side of a {@code DiffNode} to the other side.
* Two other actions are for navigating from one node to the next (previous).
* <p>
* Clients can override this method and are free to decide whether they want to call
* the inherited method.
*
* @param toolbarManager the toolbar manager for which to add the actions
*/
protected void createToolItems(ToolBarManager toolbarManager) {
}
/**
* This method is called to add actions to the viewer's context menu.
* It installs actions for expanding tree nodes, copying one side of a {@code DiffNode} to the other side.
* Clients can override this method and are free to decide whether they want to call
* the inherited method.
*
* @param manager the menu manager for which to add the actions
*/
protected void fillContextMenu(IMenuManager manager) {
if (fExpandAllAction == null) {
fExpandAllAction= new Action() {
@Override
public void run() {
expandSelection();
}
};
Utilities.initAction(fExpandAllAction, fBundle, "action.ExpandAll."); //$NON-NLS-1$
}
boolean enable= false;
ISelection selection= getSelection();
if (selection instanceof IStructuredSelection) {
Iterator<?> elements= ((IStructuredSelection) selection).iterator();
while (elements.hasNext()) {
Object element= elements.next();
if (element instanceof IDiffContainer) {
if (((IDiffContainer) element).hasChildren()) {
enable= true;
break;
}
}
}
}
fExpandAllAction.setEnabled(enable);
manager.add(fExpandAllAction);
}
/**
* Expands to infinity all items in the selection.
*
* @since 2.0
*/
protected void expandSelection() {
ISelection selection= getSelection();
if (selection instanceof IStructuredSelection) {
Iterator<?> elements= ((IStructuredSelection) selection).iterator();
while (elements.hasNext()) {
Object next= elements.next();
expandToLevel(next, ALL_LEVELS);
}
}
}
/**
* Copies one side of all {@code DiffNode}s in the current selection to the other side.
* Called from the (internal) actions for copying the sides of a {@code DiffNode}.
* Clients may override.
*
* @param leftToRight if {@code true} the left side is copied to the right side.
* If {@code false} the right side is copied to the left side
*/
protected void copySelected(boolean leftToRight) {
ISelection selection= getSelection();
if (selection instanceof IStructuredSelection) {
Iterator<?> e= ((IStructuredSelection) selection).iterator();
while (e.hasNext()) {
Object element= e.next();
if (element instanceof ICompareInput)
copyOne((ICompareInput) element, leftToRight);
}
}
}
/**
* Called to copy one side of the given node to the other.
* This default implementation delegates the call to {@code ICompareInput.copy(...)}.
* Clients may override.
*
* @param node the node to copy
* @param leftToRight if {@code true} the left side is copied to the right side.
* If {@code false} the right side is copied to the left side
*/
protected void copyOne(ICompareInput node, boolean leftToRight) {
node.copy(leftToRight);
// Update node's image.
update(new Object[] { node }, null);
}
/**
* Selects the next (or previous) node of the current selection.
* If there is no current selection the first (last) node in the tree is selected.
* Wraps around at end or beginning.
* Clients may override.
*
* @param next if {@code true} the next node is selected, otherwise the previous node
*/
protected void navigate(boolean next) {
// Fix for http://dev.eclipse.org/bugs/show_bug.cgi?id=20106
internalNavigate(next, false);
}
//---- private
/**
* Selects the next (or previous) node of the current selection.
* If there is no current selection the first (last) node in the tree is selected.
* Wraps around at end or beginning.
* Clients may override.
*
* @param next if {@code true} the next node is selected, otherwise the previous node
* @param fireOpen if {@code true} an open event is fired.
* @return {@code true} if at end (or beginning)
*/
private boolean internalNavigate(boolean next, boolean fireOpen) {
Control c= getControl();
if (!(c instanceof Tree) || c.isDisposed())
return false;
TreeItem item = getNextItem(next, true);
if (item != null) {
internalSetSelection(item, fireOpen);
}
return item == null;
}
private TreeItem getNextItem(boolean next, boolean expand) {
Control c= getControl();
if (!(c instanceof Tree) || c.isDisposed())
return null;
Tree tree= (Tree) c;
TreeItem item= null;
TreeItem children[]= tree.getSelection();
if (children != null && children.length != 0)
item= children[0];
if (item == null) {
children= tree.getItems();
if (children != null && children.length != 0) {
item= children[0];
if (item != null && item.getItemCount() <= 0) {
return item;
}
}
}
while (true) {
item= findNextPrev(item, next, expand);
if (item == null)
break;
if (item.getItemCount() <= 0)
break;
}
return item;
}
private TreeItem findNextPrev(TreeItem item, boolean next, boolean expand) {
if (item == null)
return null;
TreeItem children[]= null;
if (!next) {
TreeItem parent= item.getParentItem();
if (parent != null) {
children= parent.getItems();
} else {
children= item.getParent().getItems();
}
if (children != null && children.length > 0) {
// Go to previous child.
int index= 0;
for (; index < children.length; index++) {
if (children[index] == item)
break;
}
if (index > 0) {
item= children[index-1];
while (true) {
createChildren(item);
int n= item.getItemCount();
if (n <= 0)
break;
if (expand)
item.setExpanded(true);
item= item.getItems()[n-1];
}
// Previous.
return item;
}
}
// Go up.
item= parent;
} else {
if (expand)
item.setExpanded(true);
createChildren(item);
if (item.getItemCount() > 0) {
// Has children: go down.
children= item.getItems();
return children[0];
}
while (item != null) {
children= null;
TreeItem parent= item.getParentItem();
if (parent != null) {
children= parent.getItems();
} else {
children= item.getParent().getItems();
}
if (children != null && children.length > 0) {
// Goto next child.
int index= 0;
for (; index < children.length; index++) {
if (children[index] == item)
break;
}
if (index < children.length-1) {
// Next.
return children[index+1];
}
}
// Go up.
item= parent;
}
}
return item;
}
private void internalSetSelection(TreeItem ti, boolean fireOpen) {
if (ti != null) {
Object data= ti.getData();
if (data != null) {
// Fix for http://dev.eclipse.org/bugs/show_bug.cgi?id=20106
ISelection selection= new StructuredSelection(data);
setSelection(selection, true);
ISelection currentSelection= getSelection();
if (fireOpen && currentSelection != null && selection.equals(currentSelection)) {
fireOpen(new OpenEvent(this, selection));
}
}
}
}
private void updateActions() {
if (fExpandAllAction != null) {
fExpandAllAction.setEnabled(getSelection().isEmpty());
}
}
/*
* Fix for http://dev.eclipse.org/bugs/show_bug.cgi?id=20106
*/
private boolean internalOpen() {
ISelection selection= getSelection();
if (selection != null && !selection.isEmpty()) {
fireOpen(new OpenEvent(this, selection));
return true;
}
return false;
}
}