| /******************************************************************************* |
| * 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; |
| } |
| } |