/** * <copyright> * * Copyright (c) 2013-2017 Thales Global Services S.A.S. * 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: * Thales Global Services S.A.S. - initial API and implementation * * </copyright> */ package org.eclipse.emf.diffmerge.ui.viewers; import java.util.HashSet; import java.util.Set; import org.eclipse.compare.CompareEditorInput; import org.eclipse.compare.INavigatable; import org.eclipse.compare.IPropertyChangeNotifier; import org.eclipse.compare.contentmergeviewer.IFlushable; import org.eclipse.compare.structuremergeviewer.ICompareInput; import org.eclipse.compare.structuremergeviewer.ICompareInputChangeListener; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.emf.common.command.Command; import org.eclipse.emf.common.command.CommandStack; import org.eclipse.emf.diffmerge.api.IComparison; import org.eclipse.emf.diffmerge.api.scopes.IModelScope; import org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope; import org.eclipse.emf.diffmerge.diffdata.EComparison; import org.eclipse.emf.diffmerge.ui.EMFDiffMergeUIPlugin; import org.eclipse.emf.diffmerge.ui.Messages; import org.eclipse.emf.diffmerge.ui.diffuidata.ComparisonSelection; import org.eclipse.emf.diffmerge.ui.diffuidata.UIComparison; import org.eclipse.emf.diffmerge.ui.util.DiffMergeLabelProvider; import org.eclipse.emf.diffmerge.ui.util.MiscUtil; import org.eclipse.emf.diffmerge.ui.viewers.categories.ConflictCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.DifferenceCategorySet; import org.eclipse.emf.diffmerge.ui.viewers.categories.ElementAdditionCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.ElementRemovalCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.IgnoredDifferenceCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.MergedDifferenceCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.MoveCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.PropertyChangeCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.ThreeWayMoveCategory; import org.eclipse.emf.diffmerge.ui.viewers.categories.UnmatchedElementCategory; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.edit.domain.EditingDomain; import org.eclipse.emf.edit.ui.action.RedoAction; import org.eclipse.emf.edit.ui.action.UndoAction; import org.eclipse.emf.transaction.util.TransactionUtil; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.IWorkbenchSite; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.ActionFactory; /** * An abstract Viewer for comparisons. It only defines basic mechanisms and facilities but it does * not make assumptions about what is shown to the user or what user interactions are proposed. * Input: EMFDiffNode. * @author Olivier Constant */ public abstract class AbstractComparisonViewer extends Viewer implements IFlushable, IPropertyChangeNotifier, ICompareInputChangeListener, IAdaptable { /** The name of the "current input" property */ public static final String PROPERTY_CURRENT_INPUT = "PROPERTY_CURRENT_INPUT"; //$NON-NLS-1$ /** The optional action bars */ private IActionBars _actionBars; /** The non-null set of property change listeners */ private final Set<IPropertyChangeListener> _changeListeners; /** The main control of the viewer */ private Composite _control; /** The current input (initially null) */ private EMFDiffNode _input; /** The last command that was executed before the last save */ private Command _lastCommandBeforeSave; /** The (initially null) undo action */ private UndoAction _undoAction; /** The (initially null) redo action */ private RedoAction _redoAction; /** The optional navigatable for navigation from the workbench menu bar buttons */ private INavigatable _navigatable; /** * Constructor * @param parent_p a non-null composite * @param actionBars_p optional action bars */ public AbstractComparisonViewer(Composite parent_p, IActionBars actionBars_p) { _actionBars = actionBars_p; _changeListeners = new HashSet<IPropertyChangeListener>(1); _input = null; _lastCommandBeforeSave = null; setupUndoRedo(); _control = createControls(parent_p); hookControl(_control); registerNavigatable(_control, createNavigatable()); } /** * @see org.eclipse.compare.IPropertyChangeNotifier#addPropertyChangeListener(org.eclipse.jface.util.IPropertyChangeListener) */ public void addPropertyChangeListener(IPropertyChangeListener listener_p) { _changeListeners.add(listener_p); } /** * @see org.eclipse.compare.structuremergeviewer.ICompareInputChangeListener#compareInputChanged(org.eclipse.compare.structuremergeviewer.ICompareInput) */ public void compareInputChanged(ICompareInput source_p) { refresh(); } /** * Create the controls for this viewer and return the main control * @param parent_p a non-null composite * @return a non-null composite */ protected abstract Composite createControls(Composite parent_p); /** * Create and return the navigatable for this viewer, if relevant * @return a potentially null object */ protected INavigatable createNavigatable() { return null; } /** * Execute the given runnable that may modify the model on the given side * and ignores transactional aspects * @param runnable_p a non-null object * @param onLeft_p whether the impacted scope is the one on the left-hand side */ protected void executeOnModel(final Runnable runnable_p, boolean onLeft_p) { EMFDiffNode input = getInput(); final boolean recordChanges = input != null && input.isUndoRedoSupported(); final EditingDomain domain = getEditingDomain(onLeft_p); try { MiscUtil.executeWithBusyCursor(domain, null, runnable_p, recordChanges, getShell().getDisplay()); } catch (Exception e) { throw new OperationCanceledException(e.getLocalizedMessage()); // Trigger transaction rollback } } /** * Execute the given runnable with progress that may modify the model on the given side * and ignores transactional aspects * @param behavior_p a non-null runnable with progress * @param onLeft_p whether the impacted scope is the one on the left-hand side */ protected void executeOnModel(final IRunnableWithProgress behavior_p, boolean onLeft_p) { EMFDiffNode input = getInput(); final boolean recordChanges = input != null && input.isUndoRedoSupported(); final EditingDomain domain = getEditingDomain(onLeft_p); try { MiscUtil.executeWithProgress(domain, null, behavior_p, recordChanges); } catch (Exception e) { throw new OperationCanceledException(e.getLocalizedMessage()); // Trigger transaction rollback } } /** * Notify listeners of a property change event * @param propertyName_p the non-null name of the property * @param newValue_p the potentially null, new value of the property */ protected void firePropertyChangeEvent(String propertyName_p, Object newValue_p) { PropertyChangeEvent event = new PropertyChangeEvent( this, propertyName_p, null, newValue_p); for (IPropertyChangeListener listener : _changeListeners) { listener.propertyChange(event); } } /** * @see org.eclipse.compare.contentmergeviewer.IFlushable#flush(org.eclipse.core.runtime.IProgressMonitor) */ public void flush(IProgressMonitor monitor_p) { IComparison comparison = getComparison(); if (comparison != null) { try { if (getInput().isModified(true)) { IModelScope leftScope = comparison.getScope(getInput().getRoleForSide(true)); if (leftScope instanceof IPersistentModelScope.Editable) ((IPersistentModelScope.Editable)leftScope).save(); } if (getInput().isModified(false)) { IModelScope rightScope = comparison.getScope(getInput().getRoleForSide(false)); if (rightScope instanceof IPersistentModelScope.Editable) ((IPersistentModelScope.Editable)rightScope).save(); } firePropertyChangeEvent(CompareEditorInput.DIRTY_STATE, new Boolean(false)); if (getEditingDomain() != null) _lastCommandBeforeSave = getEditingDomain().getCommandStack().getUndoCommand(); } catch (Exception e) { MessageDialog.openError( getShell(), EMFDiffMergeUIPlugin.LABEL, Messages.ComparisonViewer_SaveFailed + e); } } } /** * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ @SuppressWarnings({ "rawtypes", "unchecked" }) // Compatibility with old versions of Eclipse public Object getAdapter(Class adapter_p) { Object result = null; if (INavigatable.class.equals(adapter_p)) result = getNavigatable(); if (result == null) result = Platform.getAdapterManager().getAdapter(this, adapter_p); return result; } /** * Return the comparison for this viewer * @return a comparison which is assumed non-null after setInput(Object) has been invoked */ protected EComparison getComparison() { UIComparison uiComparison = getUIComparison(); return uiComparison == null? null: uiComparison.getActualComparison(); } /** * @see org.eclipse.jface.viewers.Viewer#getControl() */ @Override public Composite getControl() { return _control; } /** * Return the editing domain for this viewer * @return an editing domain which may be non-null after setInput(Object) has been invoked */ protected EditingDomain getEditingDomain() { return getInput() == null? null: getInput().getEditingDomain(); } /** * Return the editing domain for the model on the given side, if any * @param onLeft_p whether the side is the left-hand side * @return a potentially null editing domain */ protected EditingDomain getEditingDomain(boolean onLeft_p) { EditingDomain result = getEditingDomain(); if (result == null) { EMFDiffNode input = getInput(); if (input != null) { // Look for possible transactional editing domain IComparison comparison = input.getActualComparison(); if (comparison != null) { IModelScope impactedScope = comparison.getScope( input.getRoleForSide(onLeft_p)); if (impactedScope instanceof IPersistentModelScope) { Resource resource = ((IPersistentModelScope)impactedScope).getHoldingResource(); if (resource != null) result = TransactionUtil.getEditingDomain(resource); } } } } return result; } /** * Return the last command that was executed before the last save * @return a potentially null command */ protected Command getLastCommandBeforeSave() { return _lastCommandBeforeSave; } /** * Return a name for the scope on the given side * @param onLeft_p whether the scope is the one on the left-hand side * @return a potentially null string */ protected String getModelName(boolean onLeft_p) { IModelScope scope = getComparison().getScope(getInput().getRoleForSide(onLeft_p)); return DiffMergeLabelProvider.getInstance().getText(scope); } /** * Return a selection provider that covers the selection of sub-viewers if any * @return a non-null selection provider */ public ISelectionProvider getMultiViewerSelectionProvider() { return this; } /** * Return the navigatable for this viewer, if any * @return a potentially null object */ public INavigatable getNavigatable() { return _navigatable; } /** * Return the resource manager for this viewer * @return a resource manager which is non-null iff input is not null */ protected ComparisonResourceManager getResourceManager() { return getInput() == null? null: getInput().getResourceManager(); } /** * Return the shell of this viewer * @return a non-null shell */ protected Shell getShell() { return getControl().getShell(); } /** * Return the workbench part site of this viewer, if any * @return a potentially null site */ protected IWorkbenchPartSite getSite() { IWorkbenchPartSite result = null; try { IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); IWorkbenchSite site = window.getActivePage().getActivePart().getSite(); if (site instanceof IWorkbenchPartSite) result = (IWorkbenchPartSite)site; } catch (Exception e) { // Just proceed } return result; } /** * Return the UI comparison for this viewer * @return a UI comparison which is assumed non-null after setInput(Object) has been invoked */ protected UIComparison getUIComparison() { return getInput() == null? null: getInput().getUIComparison(); } /** * @see org.eclipse.jface.viewers.Viewer#getInput() */ @Override public EMFDiffNode getInput() { return _input; } /** * Dispose this viewer as a reaction to the disposal of its control */ protected void handleDispose() { if (_actionBars != null) _actionBars.clearGlobalActionHandlers(); _actionBars = null; _changeListeners.clear(); _input = null; _control = null; _lastCommandBeforeSave = null; _undoAction = null; _redoAction = null; _navigatable = null; } /** * Ensure that the viewer is disposed when its control is disposed. * See ContentViewer#hookControl(Control). * @param control_p the non-null control of the viewer */ private void hookControl(Control control_p) { control_p.addDisposeListener(new DisposeListener() { /** * @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent) */ public void widgetDisposed(DisposeEvent event) { handleDispose(); } }); } /** * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object) */ @Override protected void inputChanged(Object input_p, Object oldInput_p) { if (oldInput_p instanceof ICompareInput) ((ICompareInput)oldInput_p).removeCompareInputChangeListener(this); if (_undoAction != null) { _undoAction.setEditingDomain(getEditingDomain()); _undoAction.update(); } if (_redoAction != null) { _redoAction.setEditingDomain(getEditingDomain()); _redoAction.update(); } if (_actionBars != null) _actionBars.updateActionBars(); if (input_p instanceof EMFDiffNode) { EMFDiffNode node = (EMFDiffNode)input_p; registerCategories(node); node.updateDifferenceNumbers(); node.getCategoryManager().setDefaultConfiguration(); } if (input_p instanceof ICompareInput) { final ICompareInput compareInput = (ICompareInput)input_p; compareInput.addCompareInputChangeListener(this); getControl().addDisposeListener(new DisposeListener() { /** * @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent) */ public void widgetDisposed(DisposeEvent e_p) { compareInput.removeCompareInputChangeListener(AbstractComparisonViewer.this); } }); } firePropertyChangeEvent(PROPERTY_CURRENT_INPUT, null); } /** * @see org.eclipse.jface.viewers.Viewer#refresh() */ @Override public void refresh() { refreshTools(); // Override if needed, but call super.refresh() } /** * Refresh the tools of the viewer */ protected void refreshTools() { if (_undoAction != null) _undoAction.update(); if (_redoAction != null) _redoAction.update(); if (_actionBars != null) _actionBars.updateActionBars(); } /** * Register the difference categories that are applicable to the given input diff node * @param node_p a non-null diff node */ protected void registerCategories(EMFDiffNode node_p) { // Merge process, non-pending (already handled by the user) IDifferenceCategorySet mergeCategorySet = new DifferenceCategorySet( Messages.AbstractComparisonViewer_CatSetTextMerge, Messages.AbstractComparisonViewer_CatSetDescriptionMerge); mergeCategorySet.getChildren().add(new MergedDifferenceCategory()); mergeCategorySet.getChildren().add(new IgnoredDifferenceCategory()); // Basic two-way/three-way IDifferenceCategorySet basicCategorySet = new DifferenceCategorySet( Messages.AbstractComparisonViewer_CatSetTextBasic, Messages.AbstractComparisonViewer_CatSetDescriptionBasic); basicCategorySet.getChildren().add(new UnmatchedElementCategory(true)); basicCategorySet.getChildren().add(new UnmatchedElementCategory(false)); basicCategorySet.getChildren().add(new MoveCategory()); basicCategorySet.getChildren().add(new PropertyChangeCategory()); // Basic three-way basicCategorySet.getChildren().add(new ElementAdditionCategory(true)); basicCategorySet.getChildren().add(new ElementAdditionCategory(false)); basicCategorySet.getChildren().add(new ElementRemovalCategory(true)); basicCategorySet.getChildren().add(new ElementRemovalCategory(false)); basicCategorySet.getChildren().add(new ConflictCategory()); basicCategorySet.getChildren().add(new ThreeWayMoveCategory(true)); basicCategorySet.getChildren().add(new ThreeWayMoveCategory(false)); // Registration CategoryManager catManager = node_p.getCategoryManager(); catManager.addCategories(mergeCategorySet); catManager.addCategories(basicCategorySet); } /** * Register the given navigatable for this viewer, * allowing navigation from the workbench menu bar buttons * @param control_p the non-null control of the viewer * @param navigatable_p the potentially null navigatable */ protected void registerNavigatable(Control control_p, INavigatable navigatable_p) { _navigatable = navigatable_p; if (_navigatable != null) control_p.setData(INavigatable.NAVIGATOR_PROPERTY, _navigatable); } /** * @see org.eclipse.compare.IPropertyChangeNotifier#removePropertyChangeListener(org.eclipse.jface.util.IPropertyChangeListener) */ public void removePropertyChangeListener(IPropertyChangeListener listener_p) { _changeListeners.remove(listener_p); } /** * @see org.eclipse.jface.viewers.Viewer#setInput(java.lang.Object) */ @Override public void setInput(Object input_p) { if (input_p == null || input_p instanceof EMFDiffNode) { Object oldInput = getInput(); _input = (EMFDiffNode)input_p; inputChanged(_input, oldInput); } } /** * Set up the undo/redo mechanism */ protected void setupUndoRedo() { // Undo _undoAction = new UndoAction(null) { /** * @see org.eclipse.emf.edit.ui.action.UndoAction#run() */ @Override public void run() { undoRedo(true); } /** * @see org.eclipse.emf.edit.ui.action.UndoAction#update() */ @Override public void update() { if (getEditingDomain() != null) super.update(); } }; _undoAction.setImageDescriptor(EMFDiffMergeUIPlugin.getDefault().getImageDescriptor( EMFDiffMergeUIPlugin.ImageID.UNDO)); // Redo _redoAction = new RedoAction() { /** * @see org.eclipse.emf.edit.ui.action.RedoAction#run() */ @Override public void run() { undoRedo(false); } /** * @see org.eclipse.emf.edit.ui.action.RedoAction#update() */ @Override public void update() { if (getEditingDomain() != null) super.update(); } }; _redoAction.setImageDescriptor(EMFDiffMergeUIPlugin.getDefault().getImageDescriptor( EMFDiffMergeUIPlugin.ImageID.REDO)); if (_actionBars != null) { _actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), _undoAction); _actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), _redoAction); } } /** * Apply the undo/redo mechanism * @param undo_p whether the action is undo or redo */ protected void undoRedo(final boolean undo_p) { final EditingDomain editingDomain = getEditingDomain(); if (editingDomain != null) { BusyIndicator.showWhile(getShell().getDisplay(), new Runnable() { /** * @see java.lang.Runnable#run() */ public void run() { final CommandStack stack = editingDomain.getCommandStack(); final ComparisonSelection lastActionSelection = getUIComparison().getLastActionSelection(); if (undo_p && stack.canUndo()) stack.undo(); else if (!undo_p && stack.canRedo()) stack.redo(); boolean dirty = stack.getUndoCommand() != getLastCommandBeforeSave(); firePropertyChangeEvent(CompareEditorInput.DIRTY_STATE, new Boolean(dirty)); undoRedoPerformed(undo_p); if (lastActionSelection != null) setSelection(lastActionSelection, true); } }); } } /** * Called when undo/redo has been performed, override to react * @param undo_p whether it was undo or redo */ protected void undoRedoPerformed(final boolean undo_p) { // Nothing by default } }