blob: ae1bf61e6900fa9146a5a5ac7135be98d5c241a7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2017 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
* Stefan Xenos <sxenos@gmail.com> (Google) - bug 448968 - Add diagnostic logging
*******************************************************************************/
package org.eclipse.team.ui.synchronize;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.ICompareContainer;
import org.eclipse.compare.IPropertyChangeNotifier;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.ICompareInputChangeListener;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.internal.ui.ITeamUIImages;
import org.eclipse.team.internal.ui.Policy;
import org.eclipse.team.internal.ui.TeamUIMessages;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.team.internal.ui.history.CompareFileRevisionEditorInput;
import org.eclipse.team.internal.ui.synchronize.EditableSharedDocumentAdapter.ISharedDocumentAdapterListener;
import org.eclipse.team.internal.ui.synchronize.LocalResourceSaveableComparison;
import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement;
import org.eclipse.team.internal.ui.synchronize.SaveablesCompareEditorInput;
import org.eclipse.team.ui.mapping.SaveableComparison;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.ISaveablesLifecycleListener;
import org.eclipse.ui.ISaveablesSource;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.Saveable;
import org.eclipse.ui.SaveablesLifecycleEvent;
import org.eclipse.ui.services.IDisposable;
/**
* A compare editor input that makes use of a {@link Saveable} to manage the save
* lifecycle of the editor input. If the element returned from
* {@link #createFileElement(IFile)} is used as the left side of the compare input
* and the default saveable returned from {@link #createSaveable()} is used, then
* this compare input will provide the complete save lifecycle for the local file.
* <p>
* Clients may subclass this class.
* </p>
* @since 3.3
*/
public abstract class SaveableCompareEditorInput extends CompareEditorInput implements ISaveablesSource {
private ICompareInputChangeListener compareInputChangeListener;
private final IWorkbenchPage page;
private final ListenerList<ICompareInputChangeListener> inputChangeListeners =
new ListenerList<>(ListenerList.IDENTITY);
private Saveable saveable;
private IPropertyListener propertyListener;
/**
* Returns a typed element that represents a local file. If the element
* returned from this method is used as the left contributor of the compare
* input for a {@link SaveableCompareEditorInput}, then the file will
* be properly saved when the compare editor input or viewers are saved.
*
* @param file the file
* @return a typed element that represents a local file.
*/
public static ITypedElement createFileElement(IFile file) {
return new LocalResourceTypedElement(file);
}
private static ITypedElement getFileElement(ICompareInput input, CompareEditorInput editorInput) {
if (input.getLeft() instanceof LocalResourceTypedElement) {
return input.getLeft();
}
if (editorInput instanceof CompareFileRevisionEditorInput) {
return ((CompareFileRevisionEditorInput) editorInput).getLocalElement();
}
return null;
}
private class InternalResourceSaveableComparison extends LocalResourceSaveableComparison implements ISharedDocumentAdapterListener {
private LocalResourceTypedElement lrte;
private boolean connected;
public InternalResourceSaveableComparison(
ICompareInput input, CompareEditorInput editorInput) {
super(input, editorInput, SaveableCompareEditorInput.getFileElement(input, editorInput));
ITypedElement element = SaveableCompareEditorInput.getFileElement(input, editorInput);
if (element instanceof LocalResourceTypedElement) {
lrte = (LocalResourceTypedElement) element;
if (lrte.isConnected()) {
registerSaveable(true);
} else {
lrte.setSharedDocumentListener(this);
}
}
}
@Override
protected void fireInputChange() {
SaveableCompareEditorInput.this.fireInputChange();
}
@Override
public void dispose() {
super.dispose();
if (lrte != null)
lrte.setSharedDocumentListener(null);
}
@Override
public void handleDocumentConnected() {
if (connected)
return;
connected = true;
registerSaveable(false);
if (lrte != null)
lrte.setSharedDocumentListener(null);
}
private void registerSaveable(boolean init) {
ICompareContainer container = getContainer();
IWorkbenchPart part = container.getWorkbenchPart();
if (part != null) {
ISaveablesLifecycleListener lifecycleListener= getSaveablesLifecycleListener(part);
// Remove this saveable from the lifecycle listener.
if (!init) {
lifecycleListener.handleLifecycleEvent(
new SaveablesLifecycleEvent(part, SaveablesLifecycleEvent.POST_CLOSE, new Saveable[] { this }, false));
}
// Now fix the hashing so it uses the connected document.
initializeHashing();
// Finally, add this saveable back to the listener.
lifecycleListener.handleLifecycleEvent(
new SaveablesLifecycleEvent(part, SaveablesLifecycleEvent.POST_OPEN, new Saveable[] { this }, false));
}
}
@Override
public void handleDocumentDeleted() {
// Ignore
}
@Override
public void handleDocumentDisconnected() {
// Ignore
}
@Override
public void handleDocumentFlushed() {
// Ignore
}
@Override
public void handleDocumentSaved() {
// Ignore
}
}
/**
* Creates a <code>LocalResourceCompareEditorInput</code> which is initialized with the given
* compare configuration.
* The compare configuration is passed to subsequently created viewers.
*
* @param configuration the compare configuration
* @param page the workbench page that will contain the editor
*/
public SaveableCompareEditorInput(CompareConfiguration configuration, IWorkbenchPage page) {
super(configuration);
this.page = page;
if (Policy.DEBUG_COMPARE_EDITOR_INPUT) {
logTrace("constructed"); //$NON-NLS-1$
}
}
@Override
protected void contentsCreated() {
super.contentsCreated();
if (Policy.DEBUG_COMPARE_EDITOR_INPUT) {
logTrace("compareInputChangeListener assigned"); //$NON-NLS-1$
logStackTrace();
}
compareInputChangeListener = source -> {
if (source == getCompareResult()) {
boolean closed = false;
if (source.getKind() == Differencer.NO_CHANGE) {
closed = closeEditor(true);
}
if (!closed) {
// The editor was closed either because the compare input still has changes
// or because the editor input is dirty. In either case, fire the changes
// to the registered listeners.
propogateInputChange();
}
}
};
getCompareInput().addCompareInputChangeListener(compareInputChangeListener);
if (getSaveable() instanceof SaveableComparison) {
SaveableComparison scm = (SaveableComparison) saveable;
propertyListener = (source, propId) -> {
if (propId == SaveableComparison.PROP_DIRTY) {
setDirty(saveable.isDirty());
}
};
scm.addPropertyListener(propertyListener);
}
setDirty(saveable.isDirty());
}
private ISaveablesLifecycleListener getSaveablesLifecycleListener(IWorkbenchPart part) {
ISaveablesLifecycleListener listener = Adapters.adapt(part, ISaveablesLifecycleListener.class);
if (listener == null)
listener = part.getSite().getService(ISaveablesLifecycleListener.class);
return listener;
}
@Override
protected void handleDispose() {
if (Policy.DEBUG_COMPARE_EDITOR_INPUT) {
logTrace("handleDispose()"); //$NON-NLS-1$
logStackTrace();
}
super.handleDispose();
ICompareInput compareInput = getCompareInput();
if (compareInput != null) {
if (compareInputChangeListener != null) {
compareInput.removeCompareInputChangeListener(compareInputChangeListener);
} else if (Policy.DEBUG_COMPARE_EDITOR_INPUT) {
logTrace("compareInputChangeListener is unexpectedly null!"); //$NON-NLS-1$
logStackTrace();
}
}
if (Policy.DEBUG_COMPARE_EDITOR_INPUT) {
logTrace("compareInputChangeListener = null"); //$NON-NLS-1$
}
compareInputChangeListener = null;
if (saveable instanceof SaveableComparison && propertyListener != null) {
SaveableComparison scm = (SaveableComparison) saveable;
scm.removePropertyListener(propertyListener);
propertyListener = null;
}
if (saveable instanceof LocalResourceSaveableComparison) {
LocalResourceSaveableComparison rsc = (LocalResourceSaveableComparison) saveable;
rsc.dispose();
}
if (getCompareResult() instanceof IDisposable) {
((IDisposable) getCompareResult()).dispose();
}
}
private void logStackTrace() {
new Exception("<Fake exception> in " + getClass().getName()).printStackTrace(System.out); //$NON-NLS-1$
}
private void logTrace(String string) {
System.out.println("SaveableCompareEditorInput " + System.identityHashCode(this) + ": " + string); //$NON-NLS-1$//$NON-NLS-2$
}
/**
* Prepares the compare input of this editor input. This method is not intended to be overridden of
* extended by subclasses (but is not final for backwards compatibility reasons).
* The implementation of this method in this class
* delegates the creation of the compare input to the {@link #prepareCompareInput(IProgressMonitor)}
* method which subclasses must implement.
* @see CompareEditorInput#prepareInput(IProgressMonitor)
*/
@Override
protected Object prepareInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
final ICompareInput input = prepareCompareInput(monitor);
if (input != null)
setTitle(NLS.bind(TeamUIMessages.SyncInfoCompareInput_title, new String[] { input.getName()}));
return input;
}
/**
* Method called from {@link #prepareInput(IProgressMonitor)} to obtain the input. Its purpose
* is to ensure that the input is an instance of {@link ICompareInput}.
*
* @param monitor a progress monitor
* @return the compare input
* @throws InvocationTargetException if an error occurs
* @throws InterruptedException if operation is interrupted
*/
protected abstract ICompareInput prepareCompareInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException;
/**
* Returns the compare input of this editor input.
*
* @return the compare input of this editor input
*/
protected final ICompareInput getCompareInput() {
return (ICompareInput)getCompareResult();
}
/**
* Callback from the resource saveable that is invoked when the resource is
* saved so that this input can fire a change event for its input. Subclasses
* only need this method if the left side of their compare input is
* an element returned from {@link #createFileElement(IFile)}.
*/
protected abstract void fireInputChange();
/**
* Close the editor if it is not dirty. If it is still dirty, let the
* content merge viewer handle the compare input change.
*
* @param checkForUnsavedChanges whether to check for unsaved changes
* @return <code>true</code> if the editor was closed (note that the
* close may be asynchronous)
*/
protected boolean closeEditor(boolean checkForUnsavedChanges) {
if (isSaveNeeded() && checkForUnsavedChanges)
return false;
final IWorkbenchPage page= getPage();
if (page == null)
return false;
Runnable runnable = () -> {
Shell shell= page.getWorkbenchWindow().getShell();
if (shell == null)
return;
IEditorPart part= page.findEditor(SaveableCompareEditorInput.this);
getPage().closeEditor(part, false);
};
if (Display.getCurrent() != null) {
runnable.run();
} else {
Shell shell= page.getWorkbenchWindow().getShell();
if (shell == null)
return false;
Display display= shell.getDisplay();
display.asyncExec(runnable);
}
return true;
}
private IWorkbenchPage getPage() {
if (page == null)
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
return page;
}
void propogateInputChange() {
if (!inputChangeListeners.isEmpty()) {
Object[] allListeners = inputChangeListeners.getListeners();
for (Object l : allListeners) {
final ICompareInputChangeListener listener = (ICompareInputChangeListener) l;
SafeRunner.run(new ISafeRunnable() {
@Override
public void run() throws Exception {
listener.compareInputChanged((ICompareInput)SaveableCompareEditorInput.this.getCompareResult());
}
@Override
public void handleException(Throwable exception) {
// Logged by the safe runner
}
});
}
}
}
/**
* Get the saveable that provides the save behavior for this compare editor input.
* The {@link #createSaveable()} is called to create the saveable if it does not yet exist.
* This method cannot be called until after the input is prepared (i.e. until after
* the {@link #run(IProgressMonitor)} method is called which will in turn will invoke
* {@link #prepareCompareInput(IProgressMonitor)}.
* @return saveable that provides the save behavior for this compare editor input.
*/
protected Saveable getSaveable() {
if (saveable == null) {
saveable = createSaveable();
}
return saveable;
}
/**
* Create the saveable that provides the save behavior for this compare editor input.
* By default, a saveable that handles local files is returned.
*
* @return the saveable that provides the save behavior for this compare editor input
*/
protected Saveable createSaveable() {
Object compareResult = getCompareResult();
Assert.isNotNull(compareResult, "This method cannot be called until after prepareInput is called"); //$NON-NLS-1$
return new InternalResourceSaveableComparison((ICompareInput)compareResult, this);
}
@Override
public Saveable[] getActiveSaveables() {
if (getCompareResult() == null)
return new Saveable[0];
return new Saveable[] { getSaveable() };
}
@Override
public Saveable[] getSaveables() {
return getActiveSaveables();
}
@Override
public void addCompareInputChangeListener(ICompareInput input, ICompareInputChangeListener listener) {
if (input == getCompareResult()) {
inputChangeListeners.add(listener);
} else {
super.addCompareInputChangeListener(input, listener);
}
}
@Override
public void removeCompareInputChangeListener(ICompareInput input, ICompareInputChangeListener listener) {
if (input == getCompareResult()) {
inputChangeListeners.remove(listener);
} else {
super.removeCompareInputChangeListener(input, listener);
}
}
@Override
@SuppressWarnings("unchecked")
public <T> T getAdapter(Class<T> adapter) {
if (IFile.class.equals(adapter)) {
IResource resource = Utils.getResource(getCompareResult());
if (resource instanceof IFile) {
return (T) resource;
}
}
return super.getAdapter(adapter);
}
@Override
public Image getTitleImage() {
ImageRegistry reg = TeamUIPlugin.getPlugin().getImageRegistry();
Image image = reg.get(ITeamUIImages.IMG_SYNC_VIEW);
if (image == null) {
image = getImageDescriptor().createImage();
reg.put(ITeamUIImages.IMG_SYNC_VIEW, image);
}
return image;
}
@Override
public ImageDescriptor getImageDescriptor() {
return TeamUIPlugin.getImageDescriptor(ITeamUIImages.IMG_SYNC_VIEW);
}
@Override
public Viewer findContentViewer(Viewer oldViewer, ICompareInput input, Composite parent) {
Viewer newViewer = super.findContentViewer(oldViewer, input, parent);
boolean isNewViewer= newViewer != oldViewer;
if (isNewViewer && newViewer instanceof IPropertyChangeNotifier && saveable instanceof IPropertyChangeListener) {
// Register the model for change events if appropriate.
final IPropertyChangeNotifier dsp= (IPropertyChangeNotifier) newViewer;
final IPropertyChangeListener pcl = (IPropertyChangeListener) saveable;
dsp.addPropertyChangeListener(pcl);
Control c= newViewer.getControl();
c.addDisposeListener(
e -> dsp.removePropertyChangeListener(pcl)
);
}
return newViewer;
}
@Override
public boolean canRunAsJob() {
return true;
}
@Override
public boolean isDirty() {
if (saveable != null)
return saveable.isDirty();
return super.isDirty();
}
@Override
public void registerContextMenu(final MenuManager menu, final ISelectionProvider selectionProvider) {
super.registerContextMenu(menu, selectionProvider);
final Saveable saveable = getSaveable();
if (saveable instanceof LocalResourceSaveableComparison) {
final ITypedElement element= getFileElement(getCompareInput(), this);
menu.addMenuListener(manager -> SaveablesCompareEditorInput.handleMenuAboutToShow(manager, getContainer(), saveable, element, selectionProvider));
}
}
}