/******************************************************************************* * Copyright (c) 2000, 2009 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.compare.structuremergeviewer; import java.util.Iterator; import java.util.ResourceBundle; import org.eclipse.compare.*; import org.eclipse.compare.internal.Utilities; import org.eclipse.compare.internal.patch.DiffViewerComparator; import org.eclipse.jface.action.*; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.*; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.*; /** * A tree viewer that works on objects implementing * the <code>IDiffContainer</code> and <code>IDiffElement</code> interfaces. * <p> * This class may be instantiated; it is not intended to be subclassed outside * 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 { public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // empty implementation } public boolean isDeleted(Object element) { return false; } public void dispose() { inputChanged(DiffTreeViewer.this, getInput(), null); } public Object getParent(Object element) { if (element instanceof IDiffElement) return ((IDiffElement)element).getParent(); return null; } public final boolean hasChildren(Object element) { if (element instanceof IDiffContainer) return ((IDiffContainer)element).hasChildren(); return false; } public final Object[] getChildren(Object element) { if (element instanceof IDiffContainer) return ((IDiffContainer)element).getChildren(); return new Object[0]; } public Object[] getElements(Object element) { return getChildren(element); } } /* * Takes care of swapping left and right if fLeftIsLocal * is true. */ class DiffViewerLabelProvider extends LabelProvider { public String getText(Object element) { if (element instanceof IDiffElement) return ((IDiffElement)element).getName(); return Utilities.getString(fBundle, "defaultLabel"); //$NON-NLS-1$ } public Image getImage(Object element) { if (element instanceof IDiffElement) { IDiffElement input= (IDiffElement) element; int kind= input.getKind(); if (fLeftIsLocal) { 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; } } return fCompareConfiguration.getImage(input.getImage(), kind); } return null; } } static class FilterSame extends ViewerFilter { 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; /* package */ boolean fLeftIsLocal; private IPropertyChangeListener fPropertyChangeListener; private Action fCopyLeftToRightAction; private Action fCopyRightToLeftAction; 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() { 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); } public Object getInput() { return DiffTreeViewer.this.getInput(); } public boolean openSelectedChange() { return internalOpen(); } public boolean hasChange(int changeFlag) { return getNextItem(changeFlag == INavigatable.NEXT_CHANGE, false) != null; } }; tree.setData(INavigatable.NAVIGATOR_PROPERTY, nav); fLeftIsLocal= Utilities.getBoolean(configuration, "LEFT_IS_LOCAL", false); //$NON-NLS-1$ 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= new IPropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { DiffTreeViewer.this.propertyChange(event); } }; fCompareConfiguration.addPropertyChangeListener(fPropertyChangeListener); } setContentProvider(new DiffViewerContentProvider()); setLabelProvider(new DiffViewerLabelProvider()); addSelectionChangedListener( new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent se) { 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( new IMenuListener() { public void menuAboutToShow(IMenuManager 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 */ 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. * @param event property change event that triggered call to this method */ protected void propertyChange(PropertyChangeEvent event) { // empty default implementation } 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</code> * after a new input has been set but before any controls are updated. * This default implementation calls <code>navigate(true)</code> * 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</code>s that shouldn't expand. * @param node the node to expand * @param level non-negative level, or <code>ALL_LEVELS</code> to collapse all levels of the tree */ 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</code> * to control whether a given model node should be expanded or not. * This default implementation checks whether the object is a <code>DiffNode</code> and * calls <code>dontExpand()</code> 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</code> if a node should be expanded, <code>true</code> 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</code>. Two actions * allow for copying one side of a <code>DiffNode</code> 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) { // fCopyLeftToRightAction= new Action() { // public void run() { // copySelected(true); // } // }; // Utilities.initAction(fCopyLeftToRightAction, fBundle, "action.TakeLeft."); // toolbarManager.appendToGroup("merge", fCopyLeftToRightAction); // fCopyRightToLeftAction= new Action() { // public void run() { // copySelected(false); // } // }; // Utilities.initAction(fCopyRightToLeftAction, fBundle, "action.TakeRight."); // toolbarManager.appendToGroup("merge", fCopyRightToLeftAction); // fNextAction= new Action() { // public void run() { // navigate(true); // } // }; // Utilities.initAction(fNextAction, fBundle, "action.NextDiff."); //$NON-NLS-1$ // toolbarManager.appendToGroup("navigation", fNextAction); //$NON-NLS-1$ // fPreviousAction= new Action() { // public void run() { // navigate(false); // } // }; // Utilities.initAction(fPreviousAction, fBundle, "action.PrevDiff."); //$NON-NLS-1$ // toolbarManager.appendToGroup("navigation", fPreviousAction); //$NON-NLS-1$ } /** * 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</code> 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() { 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); if (fCopyLeftToRightAction != null) manager.add(fCopyLeftToRightAction); if (fCopyRightToLeftAction != null) manager.add(fCopyRightToLeftAction); } /** * 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</code>s in the current selection to the other side. * Called from the (internal) actions for copying the sides of a <code>DiffNode</code>. * Clients may override. * * @param leftToRight if <code>true</code> the left side is copied to the right side. * If <code>false</code> 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(...)</code>. * Clients may override. * @param node the node to copy * @param leftToRight if <code>true</code> the left side is copied to the right side. * If <code>false</code> 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</code> 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</code> the next node is selected, otherwise the previous node * @param fireOpen if <code>true</code> an open event is fired. * @return <code>true</code> 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) { // goto 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 final boolean isEditable(Object element, boolean left) { if (element instanceof ICompareInput) { ICompareInput diff= (ICompareInput) element; Object side= left ? diff.getLeft() : diff.getRight(); if (side == null && diff instanceof IDiffElement) { IDiffContainer container= ((IDiffElement)diff).getParent(); if (container instanceof ICompareInput) { ICompareInput parent= (ICompareInput) container; side= left ? parent.getLeft() : parent.getRight(); } } if (side instanceof IEditableContent) return ((IEditableContent) side).isEditable(); } return false; } private void updateActions() { int leftToRight= 0; int rightToLeft= 0; ISelection selection= getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection ss= (IStructuredSelection) selection; Iterator e= ss.iterator(); while (e.hasNext()) { Object element= e.next(); if (element instanceof ICompareInput) { if (isEditable(element, false)) leftToRight++; if (isEditable(element, true)) rightToLeft++; if (leftToRight > 0 && rightToLeft > 0) break; } } if (fExpandAllAction != null) fExpandAllAction.setEnabled(selection.isEmpty()); } if (fCopyLeftToRightAction != null) fCopyLeftToRightAction.setEnabled(leftToRight > 0); if (fCopyRightToLeftAction != null) fCopyRightToLeftAction.setEnabled(rightToLeft > 0); } /* * 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; } }