| /******************************************************************************* |
| * Copyright (c) 2004, 2009 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.internal; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.core.runtime.SubProgressMonitor; |
| import org.eclipse.core.runtime.jobs.IJobChangeEvent; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.runtime.jobs.JobChangeAdapter; |
| import org.eclipse.jface.dialogs.IDialogConstants; |
| import org.eclipse.jface.dialogs.MessageDialog; |
| import org.eclipse.jface.operation.IRunnableContext; |
| import org.eclipse.jface.operation.IRunnableWithProgress; |
| import org.eclipse.jface.window.IShellProvider; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.ui.ISaveablePart; |
| import org.eclipse.ui.ISaveablePart2; |
| import org.eclipse.ui.ISaveablesLifecycleListener; |
| import org.eclipse.ui.ISaveablesSource; |
| import org.eclipse.ui.IWorkbenchPart; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.Saveable; |
| import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor; |
| import org.eclipse.ui.internal.misc.StatusUtil; |
| import org.eclipse.ui.progress.IJobRunnable; |
| import org.eclipse.ui.progress.IWorkbenchSiteProgressService; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| |
| /** |
| * Helper class for prompting to save dirty views or editors. |
| * |
| * @since 3.0.1 |
| */ |
| public class SaveableHelper { |
| |
| /** |
| * The helper must prompt. |
| */ |
| public static final int USER_RESPONSE = -1; |
| |
| private static int AutomatedResponse = USER_RESPONSE; |
| |
| /** |
| * FOR USE BY THE AUTOMATED TEST HARNESS ONLY. |
| * |
| * Sets the response to use when <code>savePart</code> is called with <code>confirm=true</code>. |
| * |
| * @param response 0 for yes, 1 for no, 2 for cancel, -1 for default (prompt) |
| */ |
| public static void testSetAutomatedResponse(int response) { |
| AutomatedResponse = response; |
| } |
| |
| /** |
| * FOR USE BY THE AUTOMATED TEST HARNESS ONLY. |
| * |
| * Sets the response to use when <code>savePart</code> is called with <code>confirm=true</code>. |
| * |
| * @return 0 for yes, 1 for no, 2 for cancel, -1 for default (prompt) |
| */ |
| public static int testGetAutomatedResponse() { |
| return AutomatedResponse; |
| } |
| |
| /** |
| * Saves the workbench part. |
| * |
| * @param saveable the part |
| * @param part the same part |
| * @param window the workbench window |
| * @param confirm request confirmation |
| * @return <code>true</code> for continue, <code>false</code> if the operation |
| * was canceled. |
| */ |
| static boolean savePart(final ISaveablePart saveable, IWorkbenchPart part, |
| IWorkbenchWindow window, boolean confirm) { |
| // Short circuit. |
| if (!saveable.isDirty()) { |
| return true; |
| } |
| |
| // If confirmation is required .. |
| if (confirm) { |
| int choice = AutomatedResponse; |
| if (choice == USER_RESPONSE) { |
| if (saveable instanceof ISaveablePart2) { |
| choice = ((ISaveablePart2)saveable).promptToSaveOnClose(); |
| } |
| if (choice == USER_RESPONSE || choice == ISaveablePart2.DEFAULT) { |
| String message = NLS.bind(WorkbenchMessages.EditorManager_saveChangesQuestion, part.getTitle()); |
| // Show a dialog. |
| String[] buttons = new String[] { |
| IDialogConstants.YES_LABEL, |
| IDialogConstants.NO_LABEL, |
| IDialogConstants.CANCEL_LABEL }; |
| MessageDialog d = new MessageDialog(window.getShell(), |
| WorkbenchMessages.Save_Resource, null, message, |
| MessageDialog.QUESTION, buttons, 0) { |
| protected int getShellStyle() { |
| return super.getShellStyle() | SWT.SHEET; |
| } |
| }; |
| choice = d.open(); |
| } |
| } |
| |
| // Branch on the user choice. |
| // The choice id is based on the order of button labels above. |
| switch (choice) { |
| case ISaveablePart2.YES : //yes |
| break; |
| case ISaveablePart2.NO : //no |
| return true; |
| default : |
| case ISaveablePart2.CANCEL : //cancel |
| return false; |
| } |
| } |
| |
| if (saveable instanceof ISaveablesSource) { |
| return saveModels((ISaveablesSource) saveable, window, confirm); |
| } |
| |
| // Create save block. |
| IRunnableWithProgress progressOp = new IRunnableWithProgress() { |
| public void run(IProgressMonitor monitor) { |
| IProgressMonitor monitorWrap = new EventLoopProgressMonitor(monitor); |
| saveable.doSave(monitorWrap); |
| } |
| }; |
| |
| // Do the save. |
| return runProgressMonitorOperation(WorkbenchMessages.Save, progressOp, window); |
| } |
| |
| /** |
| * Saves the selected dirty models from the given model source. |
| * |
| * @param modelSource the model source |
| * @param window the workbench window |
| * @param confirm |
| * @return <code>true</code> for continue, <code>false</code> if the operation |
| * was canceled or an error occurred while saving. |
| */ |
| private static boolean saveModels(ISaveablesSource modelSource, final IWorkbenchWindow window, final boolean confirm) { |
| Saveable[] selectedModels = modelSource.getActiveSaveables(); |
| final ArrayList dirtyModels = new ArrayList(); |
| for (int i = 0; i < selectedModels.length; i++) { |
| Saveable model = selectedModels[i]; |
| if (model.isDirty()) { |
| dirtyModels.add(model); |
| } |
| } |
| if (dirtyModels.isEmpty()) { |
| return true; |
| } |
| |
| // Create save block. |
| IRunnableWithProgress progressOp = new IRunnableWithProgress() { |
| public void run(IProgressMonitor monitor) { |
| IProgressMonitor monitorWrap = new EventLoopProgressMonitor(monitor); |
| monitorWrap.beginTask(WorkbenchMessages.Save, dirtyModels.size()); |
| for (Iterator i = dirtyModels.iterator(); i.hasNext();) { |
| Saveable model = (Saveable) i.next(); |
| // handle case where this model got saved as a result of saving another |
| if (!model.isDirty()) { |
| monitor.worked(1); |
| continue; |
| } |
| doSaveModel(model, new SubProgressMonitor(monitorWrap, 1), |
| window, confirm); |
| if (monitor.isCanceled()) { |
| break; |
| } |
| } |
| monitorWrap.done(); |
| } |
| }; |
| |
| // Do the save. |
| return runProgressMonitorOperation(WorkbenchMessages.Save, progressOp, window); |
| } |
| |
| /** |
| * Saves the workbench part ... this is similar to |
| * {@link SaveableHelper#savePart(ISaveablePart, IWorkbenchPart, IWorkbenchWindow, boolean) } |
| * except that the {@link ISaveablePart2#DEFAULT } case must cause the |
| * calling function to allow this part to participate in the default saving |
| * mechanism. |
| * |
| * @param saveable the part |
| * @param window the workbench window |
| * @param confirm request confirmation |
| * @return the ISaveablePart2 constant |
| */ |
| static int savePart(final ISaveablePart2 saveable, |
| IWorkbenchWindow window, boolean confirm) { |
| // Short circuit. |
| if (!saveable.isDirty()) { |
| return ISaveablePart2.YES; |
| } |
| |
| // If confirmation is required .. |
| if (confirm) { |
| int choice = AutomatedResponse; |
| if (choice == USER_RESPONSE) { |
| choice = saveable.promptToSaveOnClose(); |
| } |
| |
| // Branch on the user choice. |
| // The choice id is based on the order of button labels above. |
| if (choice!=ISaveablePart2.YES) { |
| return (choice==USER_RESPONSE?ISaveablePart2.DEFAULT:choice); |
| } |
| } |
| |
| // Create save block. |
| IRunnableWithProgress progressOp = new IRunnableWithProgress() { |
| public void run(IProgressMonitor monitor) { |
| IProgressMonitor monitorWrap = new EventLoopProgressMonitor(monitor); |
| saveable.doSave(monitorWrap); |
| } |
| }; |
| |
| // Do the save. |
| if (!runProgressMonitorOperation(WorkbenchMessages.Save, progressOp,window)) { |
| return ISaveablePart2.CANCEL; |
| } |
| return ISaveablePart2.YES; |
| } |
| |
| /** |
| * Runs a progress monitor operation. Returns true if success, false if |
| * canceled. |
| */ |
| static boolean runProgressMonitorOperation(String opName, |
| IRunnableWithProgress progressOp, IWorkbenchWindow window) { |
| return runProgressMonitorOperation(opName, progressOp, window, window); |
| } |
| |
| /** |
| * Runs a progress monitor operation. |
| * Returns true if success, false if canceled or an error occurred. |
| */ |
| static boolean runProgressMonitorOperation(String opName, |
| final IRunnableWithProgress progressOp, |
| final IRunnableContext runnableContext, final IShellProvider shellProvider) { |
| final boolean[] success = new boolean[] { false }; |
| IRunnableWithProgress runnable = new IRunnableWithProgress() { |
| public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { |
| progressOp.run(monitor); |
| // Only indicate success if the monitor wasn't canceled |
| if (!monitor.isCanceled()) |
| success[0] = true; |
| } |
| }; |
| |
| try { |
| runnableContext.run(false, true, runnable); |
| } catch (InvocationTargetException e) { |
| String title = NLS.bind(WorkbenchMessages.EditorManager_operationFailed, opName ); |
| Throwable targetExc = e.getTargetException(); |
| WorkbenchPlugin.log(title, new Status(IStatus.WARNING, |
| PlatformUI.PLUGIN_ID, 0, title, targetExc)); |
| StatusUtil.handleStatus(title, targetExc, StatusManager.SHOW, |
| shellProvider.getShell()); |
| // Fall through to return failure |
| } catch (InterruptedException e) { |
| // The user pressed cancel. Fall through to return failure |
| } catch (OperationCanceledException e) { |
| // The user pressed cancel. Fall through to return failure |
| } |
| return success[0]; |
| } |
| |
| /** |
| * Returns whether the model source needs saving. This is true if any of |
| * the active models are dirty. This logic must correspond with |
| * {@link #saveModels} above. |
| * |
| * @param modelSource |
| * the model source |
| * @return <code>true</code> if save is required, <code>false</code> |
| * otherwise |
| * @since 3.2 |
| */ |
| public static boolean needsSave(ISaveablesSource modelSource) { |
| Saveable[] selectedModels = modelSource.getActiveSaveables(); |
| for (int i = 0; i < selectedModels.length; i++) { |
| Saveable model = selectedModels[i]; |
| if (model.isDirty() && !((InternalSaveable)model).isSavingInBackground()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @param model |
| * @param progressMonitor |
| * @param shellProvider |
| * @param blockUntilSaved |
| */ |
| public static void doSaveModel(final Saveable model, |
| IProgressMonitor progressMonitor, |
| final IShellProvider shellProvider, boolean blockUntilSaved) { |
| try { |
| Job backgroundSaveJob = ((InternalSaveable)model).getBackgroundSaveJob(); |
| if (backgroundSaveJob != null) { |
| boolean canceled = waitForBackgroundSaveJob(model); |
| if (canceled) { |
| progressMonitor.setCanceled(true); |
| return; |
| } |
| // return early if the saveable is no longer dirty |
| if (!model.isDirty()) { |
| return; |
| } |
| } |
| final IJobRunnable[] backgroundSaveRunnable = new IJobRunnable[1]; |
| try { |
| SubMonitor subMonitor = SubMonitor.convert(progressMonitor, 3); |
| backgroundSaveRunnable[0] = model.doSave( |
| subMonitor.newChild(2), shellProvider); |
| if (backgroundSaveRunnable[0] == null) { |
| // no further work needs to be done |
| return; |
| } |
| if (blockUntilSaved) { |
| // for now, block on close by running the runnable in the UI |
| // thread |
| IStatus result = backgroundSaveRunnable[0].run(subMonitor |
| .newChild(1)); |
| if (!result.isOK()) { |
| StatusUtil.handleStatus(result, StatusManager.SHOW, |
| shellProvider.getShell()); |
| progressMonitor.setCanceled(true); |
| } |
| return; |
| } |
| // for the job family, we use the model object because based on |
| // the family we can display the busy state with an animated tab |
| // (see the calls to showBusyForFamily() below). |
| Job saveJob = new Job(NLS.bind( |
| WorkbenchMessages.EditorManager_backgroundSaveJobName, |
| model.getName())) { |
| public boolean belongsTo(Object family) { |
| if (family instanceof DynamicFamily) { |
| return ((DynamicFamily)family).contains(model); |
| } |
| return family.equals(model); |
| } |
| |
| protected IStatus run(IProgressMonitor monitor) { |
| return backgroundSaveRunnable[0].run(monitor); |
| } |
| }; |
| // we will need the associated parts (for disabling their UI) |
| ((InternalSaveable) model).setBackgroundSaveJob(saveJob); |
| SaveablesList saveablesList = (SaveablesList) PlatformUI |
| .getWorkbench().getService( |
| ISaveablesLifecycleListener.class); |
| final IWorkbenchPart[] parts = saveablesList |
| .getPartsForSaveable(model); |
| |
| // this will cause the parts tabs to show the ongoing background operation |
| for (int i = 0; i < parts.length; i++) { |
| IWorkbenchPart workbenchPart = parts[i]; |
| IWorkbenchSiteProgressService progressService = (IWorkbenchSiteProgressService) workbenchPart |
| .getSite().getAdapter( |
| IWorkbenchSiteProgressService.class); |
| progressService.showBusyForFamily(model); |
| } |
| model.disableUI(parts, blockUntilSaved); |
| // Add a listener for enabling the UI after the save job has |
| // finished, and for displaying an error dialog if |
| // necessary. |
| saveJob.addJobChangeListener(new JobChangeAdapter() { |
| public void done(final IJobChangeEvent event) { |
| ((InternalSaveable) model).setBackgroundSaveJob(null); |
| shellProvider.getShell().getDisplay().asyncExec( |
| new Runnable() { |
| public void run() { |
| notifySaveAction(parts); |
| model.enableUI(parts); |
| } |
| }); |
| } |
| }); |
| // Finally, we are ready to schedule the job. |
| saveJob.schedule(); |
| // the job was started - notify the save actions, |
| // this is done through the workbench windows, which |
| // we can get from the parts... |
| notifySaveAction(parts); |
| } catch (CoreException e) { |
| StatusUtil.handleStatus(e.getStatus(), StatusManager.SHOW, |
| shellProvider.getShell()); |
| progressMonitor.setCanceled(true); |
| } |
| } finally { |
| progressMonitor.done(); |
| } |
| } |
| |
| private static void notifySaveAction(final IWorkbenchPart[] parts) { |
| Set wwindows = new HashSet(); |
| for (int i = 0; i < parts.length; i++) { |
| wwindows.add(parts[i].getSite().getWorkbenchWindow()); |
| } |
| for (Iterator it = wwindows.iterator(); it.hasNext();) { |
| WorkbenchWindow wwin = (WorkbenchWindow) it.next(); |
| wwin.fireBackgroundSaveStarted(); |
| } |
| } |
| |
| /** |
| * Waits for the background save job (if any) of the given saveable to complete. |
| * This may open a progress dialog with the option to cancel. |
| * |
| * @param modelToSave |
| * @return true if the user canceled. |
| */ |
| private static boolean waitForBackgroundSaveJob(final Saveable model) { |
| List models = new ArrayList(); |
| models.add(model); |
| return waitForBackgroundSaveJobs(models); |
| } |
| |
| /** |
| * Waits for the background save jobs (if any) of the given saveables to complete. |
| * This may open a progress dialog with the option to cancel. |
| * |
| * @param modelsToSave |
| * @return true if the user canceled. |
| */ |
| public static boolean waitForBackgroundSaveJobs(final List modelsToSave) { |
| // block if any of the saveables is still saving in the background |
| try { |
| PlatformUI.getWorkbench().getProgressService().busyCursorWhile(new IRunnableWithProgress() { |
| public void run(IProgressMonitor monitor) throws InterruptedException { |
| Job.getJobManager().join(new DynamicFamily(modelsToSave), monitor); |
| } |
| }); |
| } catch (InvocationTargetException e) { |
| StatusUtil.handleStatus(e, StatusManager.SHOW | StatusManager.LOG); |
| } catch (InterruptedException e) { |
| return true; |
| } |
| // remove saveables that are no longer dirty from the list |
| for (Iterator it = modelsToSave.iterator(); it.hasNext();) { |
| Saveable model = (Saveable) it.next(); |
| if (!model.isDirty()) { |
| it.remove(); |
| } |
| } |
| return false; |
| } |
| |
| private static class DynamicFamily extends HashSet { |
| private static final long serialVersionUID = 1L; |
| public DynamicFamily(Collection collection) { |
| super(collection); |
| } |
| } |
| |
| } |