org.eclipse.ui.internal.SaveableHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ui.internal.SaveableHelper.java

Source

/*******************************************************************************
 * Copyright (c) 2004, 2010 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.
     */
    public 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);
        }
    }

}