blob: 72b782dc4e0343f30bec1681042b5313c30d8839 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2007 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.ui.operations;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IAdvancedUndoableOperation2;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.misc.StatusUtil;
import org.eclipse.ui.internal.operations.TimeTriggeredProgressMonitorDialog;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.part.MultiPageEditorSite;
import org.eclipse.ui.statushandlers.StatusManager;
/**
* <p>
* OperationHistoryActionHandler implements common behavior for the undo and
* redo actions. It supports filtering of undo or redo on a particular undo
* context. If an undo context is not specified, or there has been no history
* available for the specified undo context, then the workbench undo context
* will be used.
* </p>
* <p>
* OperationHistoryActionHandler provides an adapter in the info parameter of
* the IOperationHistory undo and redo methods that is used to get UI info for
* prompting the user during operations or operation approval. Adapters are
* provided for org.eclipse.ui.IWorkbenchWindow, org.eclipse.swt.widgets.Shell,
* org.eclipse.ui.IWorkbenchPart, org.eclipse.core.commands.IUndoContext, and
* org.eclipse.runtime.IProgressMonitor.
* </p>
* <p>
* OperationHistoryActionHandler assumes a linear undo/redo model. When the
* handler is run, the operation history is asked to perform the most recent
* undo/redo for the handler's undo context. The handler can be configured
* (using #setPruneHistory(true)) to flush the operation undo or redo history
* for the handler's undo context when there is no valid operation on top of the
* history. This avoids keeping a stale history of invalid operations. By
* default, pruning does not occur and it is assumed that clients of the
* particular undo context are pruning the history when necessary.
* </p>
*
* @since 3.1
*/
public abstract class OperationHistoryActionHandler extends Action implements
ActionFactory.IWorkbenchAction, IAdaptable {
private static final int MAX_LABEL_LENGTH = 32;
private class PartListener implements IPartListener {
/**
* @see IPartListener#partActivated(IWorkbenchPart)
*/
public void partActivated(IWorkbenchPart part) {
}
/**
* @see IPartListener#partBroughtToTop(IWorkbenchPart)
*/
public void partBroughtToTop(IWorkbenchPart part) {
}
/**
* @see IPartListener#partClosed(IWorkbenchPart)
*/
public void partClosed(IWorkbenchPart part) {
if (part.equals(site.getPart())) {
dispose();
// Special case for MultiPageEditorSite
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=103379
} else if ((site instanceof MultiPageEditorSite)
&& (part.equals(((MultiPageEditorSite) site)
.getMultiPageEditor()))) {
dispose();
}
}
/**
* @see IPartListener#partDeactivated(IWorkbenchPart)
*/
public void partDeactivated(IWorkbenchPart part) {
}
/**
* @see IPartListener#partOpened(IWorkbenchPart)
*/
public void partOpened(IWorkbenchPart part) {
}
}
private class HistoryListener implements IOperationHistoryListener {
public void historyNotification(final OperationHistoryEvent event) {
Display display = getWorkbenchWindow().getWorkbench().getDisplay();
switch (event.getEventType()) {
case OperationHistoryEvent.OPERATION_ADDED:
case OperationHistoryEvent.OPERATION_REMOVED:
case OperationHistoryEvent.UNDONE:
case OperationHistoryEvent.REDONE:
if (display != null
&& event.getOperation().hasContext(undoContext)) {
display.asyncExec(new Runnable() {
public void run() {
update();
}
});
}
break;
case OperationHistoryEvent.OPERATION_NOT_OK:
if (display != null
&& event.getOperation().hasContext(undoContext)) {
display.asyncExec(new Runnable() {
public void run() {
if (pruning) {
IStatus status = event.getStatus();
/*
* Prune the history unless we can determine
* that this was a cancelled attempt. See
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=101215
*/
if (status == null
|| status.getSeverity() != IStatus.CANCEL) {
flush();
}
// not all flushes will trigger an update so
// force it here
update();
} else {
update();
}
}
});
}
break;
case OperationHistoryEvent.OPERATION_CHANGED:
if (display != null && event.getOperation() == getOperation()) {
display.asyncExec(new Runnable() {
public void run() {
update();
}
});
}
break;
}
}
}
private boolean pruning = false;
private IPartListener partListener = new PartListener();
private IOperationHistoryListener historyListener = new HistoryListener();
private TimeTriggeredProgressMonitorDialog progressDialog;
private IUndoContext undoContext = null;
IWorkbenchPartSite site;
/**
* Construct an operation history action for the specified workbench window
* with the specified undo context.
*
* @param site -
* the workbench part site for the action.
* @param context -
* the undo context to be used
*/
OperationHistoryActionHandler(IWorkbenchPartSite site, IUndoContext context) {
// string will be reset inside action
super(""); //$NON-NLS-1$
this.site = site;
undoContext = context;
site.getPage().addPartListener(partListener);
getHistory().addOperationHistoryListener(historyListener);
// An update must be forced in case the undo limit is 0.
// see bug #89707
update();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#dispose()
*/
public void dispose() {
IOperationHistory history = getHistory();
if (history != null) {
history.removeOperationHistoryListener(historyListener);
}
if (isInvalid()) {
return;
}
site.getPage().removePartListener(partListener);
site = null;
progressDialog = null;
// We do not flush the history for our undo context because it may be
// used elsewhere. It is up to clients to clean up the history
// appropriately.
// We do null out the context to signify that this handler is no longer
// accessing the history.
undoContext = null;
}
/*
* Flush the history associated with this action.
*/
abstract void flush();
/*
* Return the string describing the command, including the binding for the
* operation label.
*/
abstract String getCommandString();
/*
* Return the string describing the command for a tooltip, including the
* binding for the operation label.
*/
abstract String getTooltipString();
/*
* Return the simple string describing the command, with no binding to any
* operation.
*/
abstract String getSimpleCommandString();
/*
* Return the simple string describing the tooltip, with no binding to any
* operation.
*/
abstract String getSimpleTooltipString();
/*
* Return the operation history we are using.
*/
IOperationHistory getHistory() {
if (PlatformUI.getWorkbench() == null) {
return null;
}
return PlatformUI.getWorkbench().getOperationSupport()
.getOperationHistory();
}
/*
* Return the current operation.
*/
abstract IUndoableOperation getOperation();
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#run()
*/
public final void run() {
if (isInvalid()) {
return;
}
Shell parent = getWorkbenchWindow().getShell();
progressDialog = new TimeTriggeredProgressMonitorDialog(parent,
getWorkbenchWindow().getWorkbench().getProgressService()
.getLongOperationTime());
IRunnableWithProgress runnable = new IRunnableWithProgress() {
public void run(IProgressMonitor pm)
throws InvocationTargetException {
try {
runCommand(pm);
} catch (ExecutionException e) {
if (pruning) {
flush();
}
throw new InvocationTargetException(e);
}
}
};
try {
boolean runInBackground = false;
if (getOperation() instanceof IAdvancedUndoableOperation2) {
runInBackground = ((IAdvancedUndoableOperation2) getOperation())
.runInBackground();
}
progressDialog.run(runInBackground, true, runnable);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (t == null) {
reportException(e);
} else {
reportException(t);
}
} catch (InterruptedException e) {
// Operation was cancelled and acknowledged by runnable with this
// exception.
// Do nothing.
} catch (OperationCanceledException e) {
// the operation was cancelled. Do nothing.
} finally {
progressDialog = null;
}
}
abstract IStatus runCommand(IProgressMonitor pm) throws ExecutionException;
/*
* (non-Javadoc)
*
* @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
*/
public Object getAdapter(Class adapter) {
if (adapter.equals(IUndoContext.class)) {
return undoContext;
}
if (adapter.equals(IProgressMonitor.class)) {
if (progressDialog != null) {
return progressDialog.getProgressMonitor();
}
}
if (site != null) {
if (adapter.equals(Shell.class)) {
return getWorkbenchWindow().getShell();
}
if (adapter.equals(IWorkbenchWindow.class)) {
return getWorkbenchWindow();
}
if (adapter.equals(IWorkbenchPart.class)) {
return site.getPart();
}
// Refer all other requests to the part itself.
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=108144
IWorkbenchPart part = site.getPart();
if (part != null) {
return Util.getAdapter(part, adapter);
}
}
return null;
}
/*
* Return the workbench window for this action handler
*/
private IWorkbenchWindow getWorkbenchWindow() {
if (site != null) {
return site.getWorkbenchWindow();
}
return null;
}
/**
* The undo and redo subclasses should implement this.
*
* @return - a boolean indicating enablement state
*/
abstract boolean shouldBeEnabled();
/**
* Set the context shown by the handler. Normally the context is set up when
* the action handler is created, but the context can also be changed
* dynamically.
*
* @param context
* the context to be used for the undo history
*/
public void setContext(IUndoContext context) {
// optimization - do nothing if there was no real change
if (context == undoContext) {
return;
}
undoContext = context;
update();
}
/**
* Specify whether the action handler should actively prune the operation
* history when invalid operations are encountered. The default value is
* <code>false</code>.
*
* @param prune
* <code>true</code> if the history should be pruned by the
* handler, and <code>false</code> if it should not.
*
*/
public void setPruneHistory(boolean prune) {
pruning = prune;
}
/**
* Update enabling and labels according to the current status of the
* operation history.
*/
public void update() {
if (isInvalid()) {
return;
}
boolean enabled = shouldBeEnabled();
String text, tooltipText;
if (enabled) {
tooltipText = NLS.bind(getTooltipString(), getOperation()
.getLabel());
text = NLS.bind(getCommandString(), shortenText(getOperation()
.getLabel()));
} else {
tooltipText = NLS.bind(
WorkbenchMessages.Operations_undoRedoCommandDisabled,
getSimpleTooltipString());
text = getSimpleCommandString();
/*
* if there is nothing to do and we are pruning the history, flush
* the history of this context.
*/
if (undoContext != null && pruning) {
flush();
}
}
setText(text);
setToolTipText(tooltipText);
setEnabled(enabled);
}
/*
* Shorten the specified command label if it is too long
*/
private String shortenText(String message) {
int length = message.length();
if (length > MAX_LABEL_LENGTH) {
StringBuffer result = new StringBuffer();
int mid = MAX_LABEL_LENGTH / 2;
result.append(message.substring(0, mid));
result.append("..."); //$NON-NLS-1$
result.append(message.substring(length - mid));
return result.toString();
}
return message;
}
/*
* Report the specified exception to the log and to the user.
*/
final void reportException(Throwable t) {
// get any nested exceptions
Throwable nestedException = StatusUtil.getCause(t);
Throwable exception = (nestedException == null) ? t : nestedException;
// Messages
String exceptionMessage = exception.getMessage();
if (exceptionMessage == null) {
exceptionMessage = WorkbenchMessages.WorkbenchWindow_exceptionMessage;
}
IStatus status = StatusUtil.newStatus(WorkbenchPlugin.PI_WORKBENCH,
exceptionMessage, exception);
// Log and show the problem
WorkbenchPlugin.log(exceptionMessage, status);
StatusUtil.handleStatus(status, StatusManager.SHOW,
getWorkbenchWindow().getShell());
}
/*
* Answer true if the receiver is not valid for running commands, accessing
* the history, etc.
*/
final boolean isInvalid() {
return undoContext == null || site == null;
}
/*
* Get the undo context that should be used.
*/
final IUndoContext getUndoContext() {
return undoContext;
}
}