001: /*******************************************************************************
002: * Copyright (c) 2005, 2006 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.util.ArrayList;
013:
014: import org.eclipse.core.commands.operations.IAdvancedUndoableOperation;
015: import org.eclipse.core.commands.operations.IOperationApprover;
016: import org.eclipse.core.commands.operations.IOperationHistory;
017: import org.eclipse.core.commands.operations.IUndoContext;
018: import org.eclipse.core.commands.operations.IUndoableOperation;
019: import org.eclipse.core.runtime.IAdaptable;
020: import org.eclipse.core.runtime.IStatus;
021: import org.eclipse.core.runtime.Status;
022: import org.eclipse.jface.dialogs.IDialogConstants;
023: import org.eclipse.jface.dialogs.MessageDialog;
024: import org.eclipse.osgi.util.NLS;
025: import org.eclipse.ui.IEditorPart;
026: import org.eclipse.ui.internal.Workbench;
027: import org.eclipse.ui.internal.WorkbenchMessages;
028: import org.eclipse.ui.internal.util.Util;
029:
030: /**
031: * <p>
032: * An operation approver that prompts the user to see if a non-local undo should
033: * proceed inside an editor. A non-local undo is detected when an operation
034: * being undone or redone affects elements other than those described by the
035: * editor itself. Clients can optionally specify a class, the preferred
036: * comparison class, that should be used when comparing objects affected by the
037: * editor with objects affected by an undo or redo operation. Comparisons
038: * between the affected objects inside the editor and those described by the
039: * operation will first be done by simply performing an equality check, using
040: * {@link java.lang.Object#equals(java.lang.Object)}. If an object described by
041: * an operation is not equal to one of the objects affected by the editor, and
042: * if it is not an instance of the preferred comparison class, but is an
043: * instance of {@link org.eclipse.core.runtime.IAdaptable}, then the operation
044: * approver will also attempt to retrieve an adapter on that object for the
045: * preferred comparison class and perform a second equality check using the
046: * adapter.
047: * </p>
048: * <p>
049: * This class may be instantiated by clients.
050: * </p>
051: *
052: *
053: * @since 3.1
054: */
055: public final class NonLocalUndoUserApprover implements
056: IOperationApprover {
057:
058: private IUndoContext context;
059:
060: private IEditorPart part;
061:
062: private Object[] elements;
063:
064: private Class affectedObjectsClass;
065:
066: private ArrayList elementsAndAdapters;
067:
068: /**
069: * Create a NonLocalUndoUserApprover associated with the specified editor
070: * and undo context
071: *
072: * @param context
073: * the undo context of operations in question.
074: * @param part
075: * the editor part that is displaying the element
076: * @param affectedObjects
077: * the objects that are affected by the editor and considered to
078: * be objects local to the editor. The objects are typically
079: * instances of the preferredComparisonClass or else provide
080: * adapters for the preferredComparisonClass, although this is
081: * not required.
082: * @param preferredComparisonClass
083: * the preferred class to be used when comparing the editor's
084: * affectedObjects with those provided by the undoable operation
085: * using
086: * {@link org.eclipse.core.commands.operations.IAdvancedUndoableOperation#getAffectedObjects()}.
087: * If the operation's affected objects are not instances of the
088: * specified class, but are instances of
089: * {@link org.eclipse.core.runtime.IAdaptable}, then an adapter
090: * for this class will be requested. The preferredComparisonClass
091: * may be <code>null</code>, which indicates that there is no
092: * expected class or adapter necessary for the comparison.
093: */
094: public NonLocalUndoUserApprover(IUndoContext context,
095: IEditorPart part, Object[] affectedObjects,
096: Class preferredComparisonClass) {
097: super ();
098: this .context = context;
099: this .part = part;
100: this .affectedObjectsClass = preferredComparisonClass;
101: this .elements = affectedObjects;
102: }
103:
104: /*
105: * (non-Javadoc)
106: *
107: * @see org.eclipse.core.commands.operations.IOperationApprover#proceedRedoing(org.eclipse.core.commands.operations.IUndoableOperation,
108: * org.eclipse.core.commands.operations.IOperationHistory,
109: * org.eclipse.core.runtime.IAdaptable)
110: */
111: public IStatus proceedRedoing(IUndoableOperation operation,
112: IOperationHistory history, IAdaptable uiInfo) {
113:
114: // return immediately if the operation is not relevant
115: if (!requiresApproval(operation, uiInfo)) {
116: return Status.OK_STATUS;
117: }
118:
119: String message = NLS.bind(
120: WorkbenchMessages.Operations_nonLocalRedoWarning,
121: operation.getLabel(), part.getEditorInput().getName());
122: return proceedWithOperation(operation, message,
123: WorkbenchMessages.Operations_discardRedo);
124: }
125:
126: /*
127: * (non-Javadoc)
128: *
129: * @see org.eclipse.core.commands.operations.IOperationApprover#proceedUndoing(org.eclipse.core.commands.operations.IUndoableOperation,
130: * org.eclipse.core.commands.operations.IOperationHistory,
131: * org.eclipse.core.runtime.IAdaptable)
132: */
133: public IStatus proceedUndoing(IUndoableOperation operation,
134: IOperationHistory history, IAdaptable uiInfo) {
135:
136: // return immediately if the operation is not relevant
137: if (!requiresApproval(operation, uiInfo)) {
138: return Status.OK_STATUS;
139: }
140:
141: String message = NLS.bind(
142: WorkbenchMessages.Operations_nonLocalUndoWarning,
143: operation.getLabel(), part.getEditorInput().getName());
144: return proceedWithOperation(operation, message,
145: WorkbenchMessages.Operations_discardUndo);
146:
147: }
148:
149: /*
150: * Determine whether the operation in question affects elements outside of
151: * the editor. If this can be determined and it does affect other elements,
152: * prompt the user as to whether the operation should proceed.
153: */
154: private IStatus proceedWithOperation(IUndoableOperation operation,
155: final String message, final String discardButton) {
156:
157: // if the operation cannot tell us about its modified elements, there's
158: // nothing we can do.
159: if (!(operation instanceof IAdvancedUndoableOperation)) {
160: return Status.OK_STATUS;
161: }
162:
163: // Obtain the operation's affected objects.
164: Object[] modifiedElements = ((IAdvancedUndoableOperation) operation)
165: .getAffectedObjects();
166:
167: // Since the operation participates in describing its affected objects,
168: // we assume for the rest of this method that an inability to
169: // determine a match implies that a non-local operation is occurring.
170: // This is a conservative assumption that provides more user prompting.
171:
172: boolean local;
173: if (modifiedElements == null) {
174: // The operation could not determine which elements are affected.
175: // Consider the operation non-local.
176: local = false;
177: } else {
178: // The operation answered some array of affected objects. Consider
179: // the operation local until a non-match is found. Note that an
180: // empty
181: // array of affected objects is considered a local change.
182: local = true;
183: for (int i = 0; i < modifiedElements.length; i++) {
184: Object modifiedElement = modifiedElements[i];
185: if (!elementsContains(modifiedElement)) {
186: // the modified element is not known by the editor
187: local = false;
188: // one last try - try to adapt the modified element if a
189: // preferred
190: // comparison class has been provided.
191: if (affectedObjectsClass != null) {
192: Object adapter = Util.getAdapter(
193: modifiedElement, affectedObjectsClass);
194: if (adapter != null
195: && elementsContains(adapter)) {
196: local = true;
197: }
198: }
199: // if the element did not match the affected objects, no
200: // need to check any others.
201: if (!local) {
202: break;
203: }
204: }
205: }
206: }
207: if (local) {
208: return Status.OK_STATUS;
209: }
210:
211: // The operation affects more than just our element. Find out if
212: // we should proceed, cancel, or discard the undo. Must be done in
213: // a syncExec because operation approval notifications may come from
214: // a background thread.
215: final int[] answer = new int[1];
216: Workbench.getInstance().getDisplay().syncExec(new Runnable() {
217: public void run() {
218: MessageDialog dialog = new MessageDialog(part.getSite()
219: .getShell(), part.getEditorInput().getName(),
220: null, message, MessageDialog.QUESTION,
221: new String[] { IDialogConstants.OK_LABEL,
222: discardButton,
223: IDialogConstants.CANCEL_LABEL }, 0); // yes is the default
224: answer[0] = dialog.open();
225: }
226: });
227: switch (answer[0]) {
228: case 0:
229: return Status.OK_STATUS;
230: case 1:
231: return IOperationHistory.OPERATION_INVALID_STATUS;
232: default:
233: // Cancel by default to include ESC key and shell close,
234: // which return SWT.DEFAULT, and any other unexpected return codes
235: return Status.CANCEL_STATUS;
236: }
237: }
238:
239: /*
240: * Answer whether this operation is relevant enough to this operation
241: * approver that it should be examined in detail.
242: */
243: private boolean requiresApproval(IUndoableOperation operation,
244: IAdaptable uiInfo) {
245: // no approval is required if the operation doesn't have our undo
246: // context
247: if (!(operation.hasContext(context))) {
248: return false;
249: }
250:
251: // no approval is required if the operation only has our context
252: if (operation.getContexts().length == 1) {
253: return false;
254: }
255:
256: // no approval is required if we can ascertain that the operation did
257: // not originate
258: // in our context.
259: if (uiInfo != null) {
260: IUndoContext originatingContext = (IUndoContext) Util
261: .getAdapter(uiInfo, IUndoContext.class);
262: if (originatingContext != null
263: && !(originatingContext.matches(context))) {
264: return false;
265: }
266: }
267:
268: return true;
269: }
270:
271: /*
272: * Return whether or not the collection of editor elements plus any of their
273: * adapters contains the specified object.
274: */
275: private boolean elementsContains(Object someObject) {
276: if (elements == null) {
277: return false;
278: }
279: if (elementsAndAdapters == null) {
280: // Compute a list of not just the elements, but any adapters they
281: // may provide on the preferred class if they are not instances of
282: // the preferred class. This is done only once.
283: elementsAndAdapters = new ArrayList(elements.length);
284: for (int i = 0; i < elements.length; i++) {
285: Object element = elements[i];
286: elementsAndAdapters.add(element);
287: if (affectedObjectsClass != null
288: && !affectedObjectsClass.isInstance(element)) {
289: Object adapter = Util.getAdapter(element,
290: affectedObjectsClass);
291: if (adapter != null) {
292: elementsAndAdapters.add(adapter);
293: }
294: }
295: }
296: }
297: for (int i = 0; i < elementsAndAdapters.size(); i++) {
298: if (elementsAndAdapters.get(i).equals(someObject)) {
299: return true;
300: }
301: }
302: return false;
303: }
304: }
|