001: /*******************************************************************************
002: * Copyright (c) 2005, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.operations;
011:
012: import java.lang.reflect.InvocationTargetException;
013:
014: import org.eclipse.core.commands.ExecutionException;
015: import org.eclipse.core.commands.operations.IAdvancedUndoableOperation2;
016: import org.eclipse.core.commands.operations.IOperationHistory;
017: import org.eclipse.core.commands.operations.IOperationHistoryListener;
018: import org.eclipse.core.commands.operations.IUndoContext;
019: import org.eclipse.core.commands.operations.IUndoableOperation;
020: import org.eclipse.core.commands.operations.OperationHistoryEvent;
021: import org.eclipse.core.runtime.IAdaptable;
022: import org.eclipse.core.runtime.IProgressMonitor;
023: import org.eclipse.core.runtime.IStatus;
024: import org.eclipse.core.runtime.OperationCanceledException;
025: import org.eclipse.jface.action.Action;
026: import org.eclipse.jface.operation.IRunnableWithProgress;
027: import org.eclipse.osgi.util.NLS;
028: import org.eclipse.swt.widgets.Display;
029: import org.eclipse.swt.widgets.Shell;
030: import org.eclipse.ui.IPartListener;
031: import org.eclipse.ui.IWorkbenchPart;
032: import org.eclipse.ui.IWorkbenchPartSite;
033: import org.eclipse.ui.IWorkbenchWindow;
034: import org.eclipse.ui.PlatformUI;
035: import org.eclipse.ui.actions.ActionFactory;
036: import org.eclipse.ui.internal.WorkbenchMessages;
037: import org.eclipse.ui.internal.WorkbenchPlugin;
038: import org.eclipse.ui.internal.misc.StatusUtil;
039: import org.eclipse.ui.internal.operations.TimeTriggeredProgressMonitorDialog;
040: import org.eclipse.ui.internal.util.Util;
041: import org.eclipse.ui.part.MultiPageEditorSite;
042: import org.eclipse.ui.statushandlers.StatusManager;
043:
044: /**
045: * <p>
046: * OperationHistoryActionHandler implements common behavior for the undo and
047: * redo actions. It supports filtering of undo or redo on a particular undo
048: * context. If an undo context is not specified, or there has been no history
049: * available for the specified undo context, then the workbench undo context
050: * will be used.
051: * </p>
052: * <p>
053: * OperationHistoryActionHandler provides an adapter in the info parameter of
054: * the IOperationHistory undo and redo methods that is used to get UI info for
055: * prompting the user during operations or operation approval. Adapters are
056: * provided for org.eclipse.ui.IWorkbenchWindow, org.eclipse.swt.widgets.Shell,
057: * org.eclipse.ui.IWorkbenchPart, org.eclipse.core.commands.IUndoContext, and
058: * org.eclipse.runtime.IProgressMonitor.
059: * </p>
060: * <p>
061: * OperationHistoryActionHandler assumes a linear undo/redo model. When the
062: * handler is run, the operation history is asked to perform the most recent
063: * undo/redo for the handler's undo context. The handler can be configured
064: * (using #setPruneHistory(true)) to flush the operation undo or redo history
065: * for the handler's undo context when there is no valid operation on top of the
066: * history. This avoids keeping a stale history of invalid operations. By
067: * default, pruning does not occur and it is assumed that clients of the
068: * particular undo context are pruning the history when necessary.
069: * </p>
070: *
071: * @since 3.1
072: */
073: public abstract class OperationHistoryActionHandler extends Action
074: implements ActionFactory.IWorkbenchAction, IAdaptable {
075:
076: private static final int MAX_LABEL_LENGTH = 32;
077:
078: private class PartListener implements IPartListener {
079: /**
080: * @see IPartListener#partActivated(IWorkbenchPart)
081: */
082: public void partActivated(IWorkbenchPart part) {
083: }
084:
085: /**
086: * @see IPartListener#partBroughtToTop(IWorkbenchPart)
087: */
088: public void partBroughtToTop(IWorkbenchPart part) {
089: }
090:
091: /**
092: * @see IPartListener#partClosed(IWorkbenchPart)
093: */
094: public void partClosed(IWorkbenchPart part) {
095: if (part.equals(site.getPart())) {
096: dispose();
097: // Special case for MultiPageEditorSite
098: // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=103379
099: } else if ((site instanceof MultiPageEditorSite)
100: && (part.equals(((MultiPageEditorSite) site)
101: .getMultiPageEditor()))) {
102: dispose();
103: }
104: }
105:
106: /**
107: * @see IPartListener#partDeactivated(IWorkbenchPart)
108: */
109: public void partDeactivated(IWorkbenchPart part) {
110: }
111:
112: /**
113: * @see IPartListener#partOpened(IWorkbenchPart)
114: */
115: public void partOpened(IWorkbenchPart part) {
116: }
117:
118: }
119:
120: private class HistoryListener implements IOperationHistoryListener {
121: public void historyNotification(
122: final OperationHistoryEvent event) {
123: Display display = getWorkbenchWindow().getWorkbench()
124: .getDisplay();
125: switch (event.getEventType()) {
126: case OperationHistoryEvent.OPERATION_ADDED:
127: case OperationHistoryEvent.OPERATION_REMOVED:
128: case OperationHistoryEvent.UNDONE:
129: case OperationHistoryEvent.REDONE:
130: if (display != null
131: && event.getOperation().hasContext(undoContext)) {
132: contextActive = true;
133: display.asyncExec(new Runnable() {
134: public void run() {
135: update();
136: }
137: });
138: }
139: break;
140: case OperationHistoryEvent.OPERATION_NOT_OK:
141: if (display != null
142: && event.getOperation().hasContext(undoContext)) {
143: contextActive = true;
144: display.asyncExec(new Runnable() {
145: public void run() {
146: if (pruning) {
147: IStatus status = event.getStatus();
148: /*
149: * Prune the history unless we can determine
150: * that this was a cancelled attempt. See
151: * https://bugs.eclipse.org/bugs/show_bug.cgi?id=101215
152: */
153: if (status == null
154: || status.getSeverity() != IStatus.CANCEL) {
155: flush();
156: }
157: // not all flushes will trigger an update so
158: // force it here
159: update();
160: } else {
161: update();
162: }
163: }
164: });
165: }
166: break;
167: case OperationHistoryEvent.OPERATION_CHANGED:
168: if (event.getOperation().hasContext(undoContext)) {
169: contextActive = true;
170: }
171: if (display != null
172: && event.getOperation() == getOperation()) {
173: display.asyncExec(new Runnable() {
174: public void run() {
175: update();
176: }
177: });
178: }
179: break;
180: default:
181: if (event.getOperation().hasContext(undoContext)) {
182: contextActive = true;
183: }
184: break;
185: }
186: }
187: }
188:
189: private boolean contextActive = false;
190:
191: private boolean pruning = false;
192:
193: private IPartListener partListener = new PartListener();
194:
195: private IOperationHistoryListener historyListener = new HistoryListener();
196:
197: private TimeTriggeredProgressMonitorDialog progressDialog;
198:
199: private IUndoContext undoContext = null;
200:
201: IWorkbenchPartSite site;
202:
203: /**
204: * Construct an operation history action for the specified workbench window
205: * with the specified undo context.
206: *
207: * @param site -
208: * the workbench part site for the action.
209: * @param context -
210: * the undo context to be used
211: */
212: OperationHistoryActionHandler(IWorkbenchPartSite site,
213: IUndoContext context) {
214: // string will be reset inside action
215: super (""); //$NON-NLS-1$
216: this .site = site;
217: undoContext = context;
218: checkUndoContext();
219: site.getPage().addPartListener(partListener);
220: getHistory().addOperationHistoryListener(historyListener);
221: // An update must be forced in case the undo limit is 0.
222: // see bug #89707
223: update();
224: }
225:
226: /*
227: * (non-Javadoc)
228: *
229: * @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#dispose()
230: */
231: public void dispose() {
232:
233: IOperationHistory history = getHistory();
234: if (history != null) {
235: history.removeOperationHistoryListener(historyListener);
236: }
237:
238: if (isInvalid()) {
239: return;
240: }
241:
242: site.getPage().removePartListener(partListener);
243: site = null;
244: progressDialog = null;
245: // We do not flush the history for our undo context because it may be
246: // used elsewhere. It is up to clients to clean up the history
247: // appropriately.
248: // We do null out the context to signify that this handler is no longer
249: // accessing the history.
250: undoContext = null;
251: }
252:
253: /*
254: * Flush the history associated with this action.
255: */
256: abstract void flush();
257:
258: /*
259: * Return the string describing the command, including the binding for the
260: * operation label.
261: */
262: abstract String getCommandString();
263:
264: /*
265: * Return the string describing the command for a tooltip, including the
266: * binding for the operation label.
267: */
268: abstract String getTooltipString();
269:
270: /*
271: * Return the simple string describing the command, with no binding to any
272: * operation.
273: */
274: abstract String getSimpleCommandString();
275:
276: /*
277: * Return the simple string describing the tooltip, with no binding to any
278: * operation.
279: */
280: abstract String getSimpleTooltipString();
281:
282: /*
283: * Return the operation history we are using.
284: */
285: IOperationHistory getHistory() {
286: if (PlatformUI.getWorkbench() == null) {
287: return null;
288: }
289:
290: return PlatformUI.getWorkbench().getOperationSupport()
291: .getOperationHistory();
292: }
293:
294: /*
295: * Return the current operation.
296: */
297: abstract IUndoableOperation getOperation();
298:
299: /*
300: * (non-Javadoc)
301: *
302: * @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#run()
303: */
304: public final void run() {
305: if (isInvalid()) {
306: return;
307: }
308:
309: Shell parent = getWorkbenchWindow().getShell();
310: progressDialog = new TimeTriggeredProgressMonitorDialog(parent,
311: getWorkbenchWindow().getWorkbench()
312: .getProgressService().getLongOperationTime());
313: IRunnableWithProgress runnable = new IRunnableWithProgress() {
314: public void run(IProgressMonitor pm)
315: throws InvocationTargetException {
316: try {
317: runCommand(pm);
318: } catch (ExecutionException e) {
319: if (pruning) {
320: flush();
321: }
322: throw new InvocationTargetException(e);
323: }
324: }
325: };
326: try {
327: boolean runInBackground = false;
328: if (getOperation() instanceof IAdvancedUndoableOperation2) {
329: runInBackground = ((IAdvancedUndoableOperation2) getOperation())
330: .runInBackground();
331: }
332: progressDialog.run(runInBackground, true, runnable);
333: } catch (InvocationTargetException e) {
334: Throwable t = e.getTargetException();
335: if (t == null) {
336: reportException(e);
337: } else {
338: reportException(t);
339: }
340: } catch (InterruptedException e) {
341: // Operation was cancelled and acknowledged by runnable with this
342: // exception.
343: // Do nothing.
344: } catch (OperationCanceledException e) {
345: // the operation was cancelled. Do nothing.
346: } finally {
347: progressDialog = null;
348: }
349: }
350:
351: abstract IStatus runCommand(IProgressMonitor pm)
352: throws ExecutionException;
353:
354: /*
355: * (non-Javadoc)
356: *
357: * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
358: */
359: public Object getAdapter(Class adapter) {
360: if (adapter.equals(IUndoContext.class)) {
361: return undoContext;
362: }
363: if (adapter.equals(IProgressMonitor.class)) {
364: if (progressDialog != null) {
365: return progressDialog.getProgressMonitor();
366: }
367: }
368: if (site != null) {
369: if (adapter.equals(Shell.class)) {
370: return getWorkbenchWindow().getShell();
371: }
372: if (adapter.equals(IWorkbenchWindow.class)) {
373: return getWorkbenchWindow();
374: }
375: if (adapter.equals(IWorkbenchPart.class)) {
376: return site.getPart();
377: }
378: // Refer all other requests to the part itself.
379: // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=108144
380: IWorkbenchPart part = site.getPart();
381: if (part != null) {
382: return Util.getAdapter(part, adapter);
383: }
384: }
385: return null;
386: }
387:
388: /*
389: * Return the workbench window for this action handler
390: */
391: private IWorkbenchWindow getWorkbenchWindow() {
392: if (site != null) {
393: return site.getWorkbenchWindow();
394: }
395: return null;
396: }
397:
398: /**
399: * The undo and redo subclasses should implement this.
400: *
401: * @return - a boolean indicating enablement state
402: */
403: abstract boolean shouldBeEnabled();
404:
405: /**
406: * Set the context shown by the handler. Normally the context is set up when
407: * the action handler is created, but the context can also be changed
408: * dynamically.
409: *
410: * @param context
411: * the context to be used for the undo history
412: */
413: public void setContext(IUndoContext context) {
414: // optimization - do nothing if there was no real change
415: if (context == undoContext) {
416: return;
417: }
418: undoContext = context;
419: checkUndoContext();
420: update();
421: }
422:
423: /**
424: * Specify whether the action handler should actively prune the operation
425: * history when invalid operations are encountered. The default value is
426: * <code>false</code>.
427: *
428: * @param prune
429: * <code>true</code> if the history should be pruned by the
430: * handler, and <code>false</code> if it should not.
431: *
432: */
433: public void setPruneHistory(boolean prune) {
434: pruning = prune;
435: }
436:
437: /**
438: * Update enabling and labels according to the current status of the
439: * operation history.
440: */
441: public void update() {
442: if (isInvalid()) {
443: return;
444: }
445:
446: boolean enabled = shouldBeEnabled();
447: String text, tooltipText;
448: if (enabled) {
449: tooltipText = NLS.bind(getTooltipString(), getOperation()
450: .getLabel());
451: text = NLS.bind(getCommandString(),
452: shortenText(getOperation().getLabel()));
453: } else {
454: tooltipText = NLS
455: .bind(
456: WorkbenchMessages.Operations_undoRedoCommandDisabled,
457: getSimpleTooltipString());
458: text = getSimpleCommandString();
459: /*
460: * if there is nothing to do and we are pruning the history, flush
461: * the history of this context.
462: */
463: if (undoContext != null && pruning) {
464: flush();
465: }
466: }
467: setText(text);
468: setToolTipText(tooltipText);
469: setEnabled(enabled);
470: }
471:
472: /*
473: * Shorten the specified command label if it is too long
474: */
475: private String shortenText(String message) {
476: int length = message.length();
477: if (length > MAX_LABEL_LENGTH) {
478: StringBuffer result = new StringBuffer();
479: int mid = MAX_LABEL_LENGTH / 2;
480: result.append(message.substring(0, mid));
481: result.append("..."); //$NON-NLS-1$
482: result.append(message.substring(length - mid));
483: return result.toString();
484: }
485: return message;
486: }
487:
488: /*
489: * Report the specified exception to the log and to the user.
490: */
491: final void reportException(Throwable t) {
492: // get any nested exceptions
493: Throwable nestedException = StatusUtil.getCause(t);
494: Throwable exception = (nestedException == null) ? t
495: : nestedException;
496:
497: // Messages
498: String exceptionMessage = exception.getMessage();
499: if (exceptionMessage == null) {
500: exceptionMessage = WorkbenchMessages.WorkbenchWindow_exceptionMessage;
501: }
502: IStatus status = StatusUtil.newStatus(
503: WorkbenchPlugin.PI_WORKBENCH, exceptionMessage,
504: exception);
505:
506: // Log and show the problem
507: WorkbenchPlugin.log(exceptionMessage, status);
508: StatusUtil.handleStatus(status, StatusManager.SHOW,
509: getWorkbenchWindow().getShell());
510: }
511:
512: /*
513: * Answer true if the receiver is not valid for running commands, accessing
514: * the history, etc.
515: */
516: final boolean isInvalid() {
517: return undoContext == null || site == null;
518: }
519:
520: /*
521: * Get the undo context that should be used.
522: */
523: final IUndoContext getUndoContext() {
524: // If no context was specified, or the specified one is not
525: // in use, use the workbench context.
526: if (undoContext == null || !contextActive) {
527: return PlatformUI.getWorkbench().getOperationSupport()
528: .getUndoContext();
529: }
530: return undoContext;
531: }
532:
533: /*
534: * The undo context has been set. Check whether there is undo or redo
535: * history available and set the contextActive flag accordingly. We check
536: * both undo and redo here because we don't ever want the undo/redo action
537: * handlers to have different values for contextActive.
538: */
539: private void checkUndoContext() {
540: if (undoContext == null) {
541: return;
542: }
543: contextActive = getHistory().getUndoOperation(undoContext) != null
544: || getHistory().getRedoOperation(undoContext) != null;
545: }
546: }
|