001: /*******************************************************************************
002: * Copyright (c) 2004, 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.internal;
011:
012: import java.lang.reflect.InvocationTargetException;
013: import java.util.ArrayList;
014: import java.util.Collection;
015: import java.util.HashSet;
016: import java.util.Iterator;
017: import java.util.List;
018: import java.util.Set;
019:
020: import org.eclipse.core.runtime.CoreException;
021: import org.eclipse.core.runtime.IProgressMonitor;
022: import org.eclipse.core.runtime.IStatus;
023: import org.eclipse.core.runtime.OperationCanceledException;
024: import org.eclipse.core.runtime.Status;
025: import org.eclipse.core.runtime.SubMonitor;
026: import org.eclipse.core.runtime.SubProgressMonitor;
027: import org.eclipse.core.runtime.jobs.IJobChangeEvent;
028: import org.eclipse.core.runtime.jobs.Job;
029: import org.eclipse.core.runtime.jobs.JobChangeAdapter;
030: import org.eclipse.jface.dialogs.IDialogConstants;
031: import org.eclipse.jface.dialogs.MessageDialog;
032: import org.eclipse.jface.operation.IRunnableContext;
033: import org.eclipse.jface.operation.IRunnableWithProgress;
034: import org.eclipse.jface.window.IShellProvider;
035: import org.eclipse.osgi.util.NLS;
036: import org.eclipse.ui.ISaveablePart;
037: import org.eclipse.ui.ISaveablePart2;
038: import org.eclipse.ui.ISaveablesLifecycleListener;
039: import org.eclipse.ui.ISaveablesSource;
040: import org.eclipse.ui.IWorkbenchPart;
041: import org.eclipse.ui.IWorkbenchWindow;
042: import org.eclipse.ui.PlatformUI;
043: import org.eclipse.ui.Saveable;
044: import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor;
045: import org.eclipse.ui.internal.misc.StatusUtil;
046: import org.eclipse.ui.progress.IJobRunnable;
047: import org.eclipse.ui.progress.IWorkbenchSiteProgressService;
048: import org.eclipse.ui.statushandlers.StatusManager;
049:
050: /**
051: * Helper class for prompting to save dirty views or editors.
052: *
053: * @since 3.0.1
054: */
055: public class SaveableHelper {
056:
057: /**
058: * The helper must prompt.
059: */
060: public static final int USER_RESPONSE = -1;
061:
062: private static int AutomatedResponse = USER_RESPONSE;
063:
064: /**
065: * FOR USE BY THE AUTOMATED TEST HARNESS ONLY.
066: *
067: * Sets the response to use when <code>savePart</code> is called with <code>confirm=true</code>.
068: *
069: * @param response 0 for yes, 1 for no, 2 for cancel, -1 for default (prompt)
070: */
071: public static void testSetAutomatedResponse(int response) {
072: AutomatedResponse = response;
073: }
074:
075: /**
076: * FOR USE BY THE AUTOMATED TEST HARNESS ONLY.
077: *
078: * Sets the response to use when <code>savePart</code> is called with <code>confirm=true</code>.
079: *
080: * @return 0 for yes, 1 for no, 2 for cancel, -1 for default (prompt)
081: */
082: public static int testGetAutomatedResponse() {
083: return AutomatedResponse;
084: }
085:
086: /**
087: * Saves the workbench part.
088: *
089: * @param saveable the part
090: * @param part the same part
091: * @param window the workbench window
092: * @param confirm request confirmation
093: * @return <code>true</code> for continue, <code>false</code> if the operation
094: * was canceled.
095: */
096: static boolean savePart(final ISaveablePart saveable,
097: IWorkbenchPart part, IWorkbenchWindow window,
098: boolean confirm) {
099: // Short circuit.
100: if (!saveable.isDirty()) {
101: return true;
102: }
103:
104: // If confirmation is required ..
105: if (confirm) {
106: int choice = AutomatedResponse;
107: if (choice == USER_RESPONSE) {
108: if (saveable instanceof ISaveablePart2) {
109: choice = ((ISaveablePart2) saveable)
110: .promptToSaveOnClose();
111: }
112: if (choice == USER_RESPONSE
113: || choice == ISaveablePart2.DEFAULT) {
114: String message = NLS
115: .bind(
116: WorkbenchMessages.EditorManager_saveChangesQuestion,
117: part.getTitle());
118: // Show a dialog.
119: String[] buttons = new String[] {
120: IDialogConstants.YES_LABEL,
121: IDialogConstants.NO_LABEL,
122: IDialogConstants.CANCEL_LABEL };
123: MessageDialog d = new MessageDialog(window
124: .getShell(),
125: WorkbenchMessages.Save_Resource, null,
126: message, MessageDialog.QUESTION, buttons, 0);
127: choice = d.open();
128: }
129: }
130:
131: // Branch on the user choice.
132: // The choice id is based on the order of button labels above.
133: switch (choice) {
134: case ISaveablePart2.YES: //yes
135: break;
136: case ISaveablePart2.NO: //no
137: return true;
138: default:
139: case ISaveablePart2.CANCEL: //cancel
140: return false;
141: }
142: }
143:
144: if (saveable instanceof ISaveablesSource) {
145: return saveModels((ISaveablesSource) saveable, window,
146: confirm);
147: }
148:
149: // Create save block.
150: IRunnableWithProgress progressOp = new IRunnableWithProgress() {
151: public void run(IProgressMonitor monitor) {
152: IProgressMonitor monitorWrap = new EventLoopProgressMonitor(
153: monitor);
154: saveable.doSave(monitorWrap);
155: }
156: };
157:
158: // Do the save.
159: return runProgressMonitorOperation(WorkbenchMessages.Save,
160: progressOp, window);
161: }
162:
163: /**
164: * Saves the selected dirty models from the given model source.
165: *
166: * @param modelSource the model source
167: * @param window the workbench window
168: * @param confirm
169: * @return <code>true</code> for continue, <code>false</code> if the operation
170: * was canceled or an error occurred while saving.
171: */
172: private static boolean saveModels(ISaveablesSource modelSource,
173: final IWorkbenchWindow window, final boolean confirm) {
174: Saveable[] selectedModels = modelSource.getActiveSaveables();
175: final ArrayList dirtyModels = new ArrayList();
176: for (int i = 0; i < selectedModels.length; i++) {
177: Saveable model = selectedModels[i];
178: if (model.isDirty()) {
179: dirtyModels.add(model);
180: }
181: }
182: if (dirtyModels.isEmpty()) {
183: return true;
184: }
185:
186: // Create save block.
187: IRunnableWithProgress progressOp = new IRunnableWithProgress() {
188: public void run(IProgressMonitor monitor) {
189: IProgressMonitor monitorWrap = new EventLoopProgressMonitor(
190: monitor);
191: monitorWrap.beginTask(WorkbenchMessages.Save,
192: dirtyModels.size());
193: for (Iterator i = dirtyModels.iterator(); i.hasNext();) {
194: Saveable model = (Saveable) i.next();
195: // handle case where this model got saved as a result of saving another
196: if (!model.isDirty()) {
197: monitor.worked(1);
198: continue;
199: }
200: doSaveModel(model, new SubProgressMonitor(
201: monitorWrap, 1), window, confirm);
202: if (monitor.isCanceled()) {
203: break;
204: }
205: }
206: monitorWrap.done();
207: }
208: };
209:
210: // Do the save.
211: return runProgressMonitorOperation(WorkbenchMessages.Save,
212: progressOp, window);
213: }
214:
215: /**
216: * Saves the workbench part ... this is similar to
217: * {@link SaveableHelper#savePart(ISaveablePart, IWorkbenchPart, IWorkbenchWindow, boolean) }
218: * except that the {@link ISaveablePart2#DEFAULT } case must cause the
219: * calling function to allow this part to participate in the default saving
220: * mechanism.
221: *
222: * @param saveable the part
223: * @param window the workbench window
224: * @param confirm request confirmation
225: * @return the ISaveablePart2 constant
226: */
227: static int savePart(final ISaveablePart2 saveable,
228: IWorkbenchWindow window, boolean confirm) {
229: // Short circuit.
230: if (!saveable.isDirty()) {
231: return ISaveablePart2.YES;
232: }
233:
234: // If confirmation is required ..
235: if (confirm) {
236: int choice = AutomatedResponse;
237: if (choice == USER_RESPONSE) {
238: choice = saveable.promptToSaveOnClose();
239: }
240:
241: // Branch on the user choice.
242: // The choice id is based on the order of button labels above.
243: if (choice != ISaveablePart2.YES) {
244: return (choice == USER_RESPONSE ? ISaveablePart2.DEFAULT
245: : choice);
246: }
247: }
248:
249: // Create save block.
250: IRunnableWithProgress progressOp = new IRunnableWithProgress() {
251: public void run(IProgressMonitor monitor) {
252: IProgressMonitor monitorWrap = new EventLoopProgressMonitor(
253: monitor);
254: saveable.doSave(monitorWrap);
255: }
256: };
257:
258: // Do the save.
259: if (!runProgressMonitorOperation(WorkbenchMessages.Save,
260: progressOp, window)) {
261: return ISaveablePart2.CANCEL;
262: }
263: return ISaveablePart2.YES;
264: }
265:
266: /**
267: * Runs a progress monitor operation. Returns true if success, false if
268: * canceled.
269: */
270: static boolean runProgressMonitorOperation(String opName,
271: IRunnableWithProgress progressOp, IWorkbenchWindow window) {
272: return runProgressMonitorOperation(opName, progressOp, window,
273: window);
274: }
275:
276: /**
277: * Runs a progress monitor operation.
278: * Returns true if success, false if canceled or an error occurred.
279: */
280: static boolean runProgressMonitorOperation(String opName,
281: final IRunnableWithProgress progressOp,
282: final IRunnableContext runnableContext,
283: final IShellProvider shellProvider) {
284: final boolean[] success = new boolean[] { false };
285: IRunnableWithProgress runnable = new IRunnableWithProgress() {
286: public void run(IProgressMonitor monitor)
287: throws InvocationTargetException,
288: InterruptedException {
289: progressOp.run(monitor);
290: // Only indicate success if the monitor wasn't canceled
291: if (!monitor.isCanceled())
292: success[0] = true;
293: }
294: };
295:
296: try {
297: runnableContext.run(false, true, runnable);
298: } catch (InvocationTargetException e) {
299: String title = NLS.bind(
300: WorkbenchMessages.EditorManager_operationFailed,
301: opName);
302: Throwable targetExc = e.getTargetException();
303: WorkbenchPlugin.log(title, new Status(IStatus.WARNING,
304: PlatformUI.PLUGIN_ID, 0, title, targetExc));
305: StatusUtil.handleStatus(title, targetExc,
306: StatusManager.SHOW, shellProvider.getShell());
307: // Fall through to return failure
308: } catch (InterruptedException e) {
309: // The user pressed cancel. Fall through to return failure
310: } catch (OperationCanceledException e) {
311: // The user pressed cancel. Fall through to return failure
312: }
313: return success[0];
314: }
315:
316: /**
317: * Returns whether the model source needs saving. This is true if any of
318: * the active models are dirty. This logic must correspond with
319: * {@link #saveModels} above.
320: *
321: * @param modelSource
322: * the model source
323: * @return <code>true</code> if save is required, <code>false</code>
324: * otherwise
325: * @since 3.2
326: */
327: public static boolean needsSave(ISaveablesSource modelSource) {
328: Saveable[] selectedModels = modelSource.getActiveSaveables();
329: for (int i = 0; i < selectedModels.length; i++) {
330: Saveable model = selectedModels[i];
331: if (model.isDirty()
332: && !((InternalSaveable) model)
333: .isSavingInBackground()) {
334: return true;
335: }
336: }
337: return false;
338: }
339:
340: /**
341: * @param model
342: * @param progressMonitor
343: * @param shellProvider
344: * @param blockUntilSaved
345: */
346: public static void doSaveModel(final Saveable model,
347: IProgressMonitor progressMonitor,
348: final IShellProvider shellProvider, boolean blockUntilSaved) {
349: try {
350: Job backgroundSaveJob = ((InternalSaveable) model)
351: .getBackgroundSaveJob();
352: if (backgroundSaveJob != null) {
353: boolean canceled = waitForBackgroundSaveJob(model);
354: if (canceled) {
355: progressMonitor.setCanceled(true);
356: return;
357: }
358: // return early if the saveable is no longer dirty
359: if (!model.isDirty()) {
360: return;
361: }
362: }
363: final IJobRunnable[] backgroundSaveRunnable = new IJobRunnable[1];
364: try {
365: SubMonitor subMonitor = SubMonitor.convert(
366: progressMonitor, 3);
367: backgroundSaveRunnable[0] = model.doSave(subMonitor
368: .newChild(2), shellProvider);
369: if (backgroundSaveRunnable[0] == null) {
370: // no further work needs to be done
371: return;
372: }
373: if (blockUntilSaved) {
374: // for now, block on close by running the runnable in the UI
375: // thread
376: IStatus result = backgroundSaveRunnable[0]
377: .run(subMonitor.newChild(1));
378: if (!result.isOK()) {
379: StatusUtil.handleStatus(result,
380: StatusManager.SHOW, shellProvider
381: .getShell());
382: progressMonitor.setCanceled(true);
383: }
384: return;
385: }
386: // for the job family, we use the model object because based on
387: // the family we can display the busy state with an animated tab
388: // (see the calls to showBusyForFamily() below).
389: Job saveJob = new Job(
390: NLS
391: .bind(
392: WorkbenchMessages.EditorManager_backgroundSaveJobName,
393: model.getName())) {
394: public boolean belongsTo(Object family) {
395: if (family instanceof DynamicFamily) {
396: return ((DynamicFamily) family)
397: .contains(model);
398: }
399: return family.equals(model);
400: }
401:
402: protected IStatus run(IProgressMonitor monitor) {
403: return backgroundSaveRunnable[0].run(monitor);
404: }
405: };
406: // we will need the associated parts (for disabling their UI)
407: ((InternalSaveable) model)
408: .setBackgroundSaveJob(saveJob);
409: SaveablesList saveablesList = (SaveablesList) PlatformUI
410: .getWorkbench().getService(
411: ISaveablesLifecycleListener.class);
412: final IWorkbenchPart[] parts = saveablesList
413: .getPartsForSaveable(model);
414:
415: // this will cause the parts tabs to show the ongoing background operation
416: for (int i = 0; i < parts.length; i++) {
417: IWorkbenchPart workbenchPart = parts[i];
418: IWorkbenchSiteProgressService progressService = (IWorkbenchSiteProgressService) workbenchPart
419: .getSite()
420: .getAdapter(
421: IWorkbenchSiteProgressService.class);
422: progressService.showBusyForFamily(model);
423: }
424: model.disableUI(parts, blockUntilSaved);
425: // Add a listener for enabling the UI after the save job has
426: // finished, and for displaying an error dialog if
427: // necessary.
428: saveJob.addJobChangeListener(new JobChangeAdapter() {
429: public void done(final IJobChangeEvent event) {
430: ((InternalSaveable) model)
431: .setBackgroundSaveJob(null);
432: shellProvider.getShell().getDisplay()
433: .asyncExec(new Runnable() {
434: public void run() {
435: notifySaveAction(parts);
436: model.enableUI(parts);
437: }
438: });
439: }
440: });
441: // Finally, we are ready to schedule the job.
442: saveJob.schedule();
443: // the job was started - notify the save actions,
444: // this is done through the workbench windows, which
445: // we can get from the parts...
446: notifySaveAction(parts);
447: } catch (CoreException e) {
448: StatusUtil.handleStatus(e.getStatus(),
449: StatusManager.SHOW, shellProvider.getShell());
450: progressMonitor.setCanceled(true);
451: }
452: } finally {
453: progressMonitor.done();
454: }
455: }
456:
457: private static void notifySaveAction(final IWorkbenchPart[] parts) {
458: Set wwindows = new HashSet();
459: for (int i = 0; i < parts.length; i++) {
460: wwindows.add(parts[i].getSite().getWorkbenchWindow());
461: }
462: for (Iterator it = wwindows.iterator(); it.hasNext();) {
463: WorkbenchWindow wwin = (WorkbenchWindow) it.next();
464: wwin.fireBackgroundSaveStarted();
465: }
466: }
467:
468: /**
469: * Waits for the background save job (if any) of the given saveable to complete.
470: * This may open a progress dialog with the option to cancel.
471: *
472: * @param modelToSave
473: * @return true if the user canceled.
474: */
475: private static boolean waitForBackgroundSaveJob(final Saveable model) {
476: List models = new ArrayList();
477: models.add(model);
478: return waitForBackgroundSaveJobs(models);
479: }
480:
481: /**
482: * Waits for the background save jobs (if any) of the given saveables to complete.
483: * This may open a progress dialog with the option to cancel.
484: *
485: * @param modelsToSave
486: * @return true if the user canceled.
487: */
488: public static boolean waitForBackgroundSaveJobs(
489: final List modelsToSave) {
490: // block if any of the saveables is still saving in the background
491: try {
492: PlatformUI.getWorkbench().getProgressService()
493: .busyCursorWhile(new IRunnableWithProgress() {
494: public void run(IProgressMonitor monitor)
495: throws InterruptedException {
496: Job.getJobManager().join(
497: new DynamicFamily(modelsToSave),
498: monitor);
499: }
500: });
501: } catch (InvocationTargetException e) {
502: StatusUtil.handleStatus(e, StatusManager.SHOW
503: | StatusManager.LOG);
504: } catch (InterruptedException e) {
505: return true;
506: }
507: // remove saveables that are no longer dirty from the list
508: for (Iterator it = modelsToSave.iterator(); it.hasNext();) {
509: Saveable model = (Saveable) it.next();
510: if (!model.isDirty()) {
511: it.remove();
512: }
513: }
514: return false;
515: }
516:
517: private static class DynamicFamily extends HashSet {
518: private static final long serialVersionUID = 1L;
519:
520: public DynamicFamily(Collection collection) {
521: super(collection);
522: }
523: }
524:
525: }
|