/******************************************************************************* * Copyright (c) 2006, 2007 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.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.AssertionFailedException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.MessageDialogWithToggle; import org.eclipse.jface.operation.IRunnableContext; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.window.IShellProvider; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; 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.IWorkbenchPreferenceConstants; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.Saveable; import org.eclipse.ui.SaveablesLifecycleEvent; import org.eclipse.ui.dialogs.ListSelectionDialog; import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor; import org.eclipse.ui.internal.misc.StatusUtil; import org.eclipse.ui.internal.util.PrefUtil; import org.eclipse.ui.model.WorkbenchPartLabelProvider; /** * The model manager maintains a list of open saveable models. * * @see Saveable * @see ISaveablesSource * * @since 3.2 */ public class SaveablesList implements ISaveablesLifecycleListener { private ListenerList listeners = new ListenerList(); // event source (mostly ISaveablesSource) -> Set of Saveable private Map modelMap = new HashMap(); // reference counting map, Saveable -> Integer private Map modelRefCounts = new HashMap(); private Set nonPartSources = new HashSet(); /** * Returns the list of open models managed by this model manager. * * @return a list of models */ public Saveable[] getOpenModels() { Set allDistinctModels = new HashSet(); Iterator saveables = modelMap.values().iterator(); while (saveables.hasNext()) allDistinctModels.addAll((Set)saveables.next()); return (Saveable[]) allDistinctModels.toArray( new Saveable[allDistinctModels.size()]); } // returns true if this model has not yet been in getModels() private boolean addModel(Object source, Saveable model) { if (model == null) { logWarning( "Ignored attempt to add invalid saveable", source, model); //$NON-NLS-1$ return false; } boolean result = false; Set modelsForSource = (Set) modelMap.get(source); if (modelsForSource == null) { modelsForSource = new HashSet(); modelMap.put(source, modelsForSource); } if (modelsForSource.add(model)) { result = incrementRefCount(modelRefCounts, model); } else { logWarning( "Ignored attempt to add saveable that was already registered", source, model); //$NON-NLS-1$ } return result; } /** * returns true if the given key was added for the first time * * @param referenceMap * @param key * @return true if the ref count of the given key is now 1 */ private boolean incrementRefCount(Map referenceMap, Object key) { boolean result = false; Integer refCount = (Integer) referenceMap.get(key); if (refCount == null) { result = true; refCount = new Integer(0); } referenceMap.put(key, new Integer(refCount.intValue() + 1)); return result; } /** * returns true if the given key has been removed * * @param referenceMap * @param key * @return true if the ref count of the given key was 1 */ private boolean decrementRefCount(Map referenceMap, Object key) { boolean result = false; Integer refCount = (Integer) referenceMap.get(key); Assert.isTrue(refCount != null); if (refCount.intValue() == 1) { referenceMap.remove(key); result = true; } else { referenceMap.put(key, new Integer(refCount.intValue() - 1)); } return result; } // returns true if this model was removed from getModels(); private boolean removeModel(Object source, Saveable model) { boolean result = false; Set modelsForSource = (Set) modelMap.get(source); if (modelsForSource == null) { logWarning( "Ignored attempt to remove a saveable when no saveables were known", source, model); //$NON-NLS-1$ } else { if (modelsForSource.remove(model)) { result = decrementRefCount(modelRefCounts, model); if (modelsForSource.isEmpty()) { modelMap.remove(source); } } else { logWarning( "Ignored attempt to remove a saveable that was not registered", source, model); //$NON-NLS-1$ } } return result; } private void logWarning(String message, Object source, Saveable model) { // create a new exception AssertionFailedException assertionFailedException = new AssertionFailedException("unknown saveable: " + model //$NON-NLS-1$ + " from part: " + source); //$NON-NLS-1$ // record the current stack trace to help with debugging assertionFailedException.fillInStackTrace(); WorkbenchPlugin.log(StatusUtil.newStatus(IStatus.WARNING, message, assertionFailedException)); } /** * This implementation of handleModelLifecycleEvent must be called by * implementers of ISaveablesSource whenever the list of models of the model * source changes, or when the dirty state of models changes. The * ISaveablesSource instance must be passed as the source of the event * object. * <p> * This method may also be called by objects that hold on to models but are * not workbench parts. In this case, the event source must be set to an * object that is not an instanceof IWorkbenchPart. * </p> * <p> * Corresponding open and close events must originate from the same * (identical) event source. * </p> * <p> * This method must be called on the UI thread. * </p> */ public void handleLifecycleEvent(SaveablesLifecycleEvent event) { if (!(event.getSource() instanceof IWorkbenchPart)) { // just update the set of non-part sources. No prompting necessary. // See bug 139004. updateNonPartSource((ISaveablesSource) event.getSource()); return; } Saveable[] modelArray = event.getSaveables(); switch (event.getEventType()) { case SaveablesLifecycleEvent.POST_OPEN: addModels(event.getSource(), modelArray); break; case SaveablesLifecycleEvent.PRE_CLOSE: Saveable[] models = event.getSaveables(); Map modelsDecrementing = new HashMap(); Set modelsClosing = new HashSet(); for (int i = 0; i < models.length; i++) { incrementRefCount(modelsDecrementing, models[i]); } fillModelsClosing(modelsClosing, modelsDecrementing); boolean canceled = promptForSavingIfNecessary(PlatformUI .getWorkbench().getActiveWorkbenchWindow(), modelsClosing, modelsDecrementing, !event.isForce()); if (canceled) { event.setVeto(true); } break; case SaveablesLifecycleEvent.POST_CLOSE: removeModels(event.getSource(), modelArray); break; case SaveablesLifecycleEvent.DIRTY_CHANGED: fireModelLifecycleEvent(new SaveablesLifecycleEvent(this, event .getEventType(), event.getSaveables(), false)); break; } } /** * Updates the set of non-part saveables sources. * @param source */ private void updateNonPartSource(ISaveablesSource source) { Saveable[] saveables = source.getSaveables(); if (saveables.length == 0) { nonPartSources.remove(source); } else { nonPartSources.add(source); } } /** * @param source * @param modelArray */ private void removeModels(Object source, Saveable[] modelArray) { List removed = new ArrayList(); for (int i = 0; i < modelArray.length; i++) { Saveable model = modelArray[i]; if (removeModel(source, model)) { removed.add(model); } } if (removed.size() > 0) { fireModelLifecycleEvent(new SaveablesLifecycleEvent(this, SaveablesLifecycleEvent.POST_OPEN, (Saveable[]) removed .toArray(new Saveable[removed.size()]), false)); } } /** * @param source * @param modelArray */ private void addModels(Object source, Saveable[] modelArray) { List added = new ArrayList(); for (int i = 0; i < modelArray.length; i++) { Saveable model = modelArray[i]; if (addModel(source, model)) { added.add(model); } } if (added.size() > 0) { fireModelLifecycleEvent(new SaveablesLifecycleEvent(this, SaveablesLifecycleEvent.POST_OPEN, (Saveable[]) added .toArray(new Saveable[added.size()]), false)); } } /** * @param event */ private void fireModelLifecycleEvent(SaveablesLifecycleEvent event) { Object[] listenerArray = listeners.getListeners(); for (int i = 0; i < listenerArray.length; i++) { ((ISaveablesLifecycleListener) listenerArray[i]) .handleLifecycleEvent(event); } } /** * Adds the given listener to the list of listeners. Has no effect if the * same (identical) listener has already been added. The listener will be * notified about changes to the models managed by this model manager. Event * types include: <br> * POST_OPEN when models were added to the list of models <br> * POST_CLOSE when models were removed from the list of models <br> * DIRTY_CHANGED when the dirty state of models changed * <p> * Listeners should ignore all other event types, including PRE_CLOSE. There * is no guarantee that listeners are notified before models are closed. * * @param listener */ public void addModelLifecycleListener(ISaveablesLifecycleListener listener) { listeners.add(listener); } /** * Removes the given listener from the list of listeners. Has no effect if * the given listener is not contained in the list. * * @param listener */ public void removeModelLifecycleListener(ISaveablesLifecycleListener listener) { listeners.remove(listener); } /** * @param partsToClose * @param save * @param window * @return the post close info to be passed to postClose */ public Object preCloseParts(List partsToClose, boolean save, final IWorkbenchWindow window) { // reference count (how many occurrences of a model will go away?) PostCloseInfo postCloseInfo = new PostCloseInfo(); for (Iterator it = partsToClose.iterator(); it.hasNext();) { IWorkbenchPart part = (IWorkbenchPart) it.next(); postCloseInfo.partsClosing.add(part); if (part instanceof ISaveablePart) { ISaveablePart saveablePart = (ISaveablePart) part; if (save && !saveablePart.isSaveOnCloseNeeded()) { // pretend for now that this part is not closing continue; } } if (save && part instanceof ISaveablePart2) { ISaveablePart2 saveablePart2 = (ISaveablePart2) part; // TODO show saveablePart2 before prompting, see // EditorManager.saveAll int response = SaveableHelper.savePart(saveablePart2, window, true); if (response == ISaveablePart2.CANCEL) { // user canceled return null; } else if (response != ISaveablePart2.DEFAULT) { // only include this part in the following logic if it returned // DEFAULT continue; } } Saveable[] modelsFromSource = getSaveables(part); for (int i = 0; i < modelsFromSource.length; i++) { incrementRefCount(postCloseInfo.modelsDecrementing, modelsFromSource[i]); } } fillModelsClosing(postCloseInfo.modelsClosing, postCloseInfo.modelsDecrementing); if (save) { boolean canceled = promptForSavingIfNecessary(window, postCloseInfo.modelsClosing, postCloseInfo.modelsDecrementing, true); if (canceled) { return null; } } return postCloseInfo; } /** * @param window * @param modelsClosing * @param canCancel * @return true if the user canceled */ private boolean promptForSavingIfNecessary(final IWorkbenchWindow window, Set modelsClosing, Map modelsDecrementing, boolean canCancel) { List modelsToOptionallySave = new ArrayList(); for (Iterator it = modelsDecrementing.keySet().iterator(); it.hasNext();) { Saveable modelDecrementing = (Saveable) it.next(); if (modelDecrementing.isDirty() && !modelsClosing.contains(modelDecrementing)) { modelsToOptionallySave.add(modelDecrementing); } } boolean shouldCancel = modelsToOptionallySave.isEmpty() ? false : promptForSaving(modelsToOptionallySave, window, window, canCancel, true); if (shouldCancel) { return true; } List modelsToSave = new ArrayList(); for (Iterator it = modelsClosing.iterator(); it.hasNext();) { Saveable modelClosing = (Saveable) it.next(); if (modelClosing.isDirty()) { modelsToSave.add(modelClosing); } } return modelsToSave.isEmpty() ? false : promptForSaving(modelsToSave, window, window, canCancel, false); } /** * @param modelsClosing * @param modelsDecrementing */ private void fillModelsClosing(Set modelsClosing, Map modelsDecrementing) { for (Iterator it = modelsDecrementing.keySet().iterator(); it.hasNext();) { Saveable model = (Saveable) it.next(); if (modelsDecrementing.get(model).equals(modelRefCounts.get(model))) { modelsClosing.add(model); } } } /** * Prompt the user to save the given saveables. * @param modelsToSave the saveables to be saved * @param shellProvider the provider used to obtain a shell in prompting is * required. Clients can use a workbench window for this. * @param runnableContext a runnable context that will be used to provide a * progress monitor while the save is taking place. Clients can * use a workbench window for this. * @param canCancel whether the operation can be canceled * @param stillOpenElsewhere whether the models are referenced by open parts * @return true if the user canceled */ public boolean promptForSaving(List modelsToSave, final IShellProvider shellProvider, IRunnableContext runnableContext, final boolean canCancel, boolean stillOpenElsewhere) { // Save parts, exit the method if cancel is pressed. if (modelsToSave.size() > 0) { boolean canceled = SaveableHelper.waitForBackgroundSaveJobs(modelsToSave); if (canceled) { return true; } IPreferenceStore apiPreferenceStore = PrefUtil.getAPIPreferenceStore(); boolean dontPrompt = stillOpenElsewhere && !apiPreferenceStore.getBoolean(IWorkbenchPreferenceConstants.PROMPT_WHEN_SAVEABLE_STILL_OPEN); if (dontPrompt) { modelsToSave.clear(); return false; } else if (modelsToSave.size() == 1) { Saveable model = (Saveable) modelsToSave.get(0); // Show a dialog. String[] buttons; if(canCancel) { buttons = new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL, IDialogConstants.CANCEL_LABEL }; } else { buttons = new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL}; } // don't save if we don't prompt int choice = ISaveablePart2.NO; MessageDialog dialog; if (stillOpenElsewhere) { String message = NLS .bind( WorkbenchMessages.EditorManager_saveChangesOptionallyQuestion, model.getName()); MessageDialogWithToggle dialogWithToggle = new MessageDialogWithToggle(shellProvider.getShell(), WorkbenchMessages.Save_Resource, null, message, MessageDialog.QUESTION, buttons, 0, WorkbenchMessages.EditorManager_closeWithoutPromptingOption, false) { protected int getShellStyle() { return (canCancel ? SWT.CLOSE : SWT.NONE) | SWT.TITLE | SWT.BORDER | SWT.APPLICATION_MODAL | getDefaultOrientation(); } }; dialog = dialogWithToggle; } else { String message = NLS .bind( WorkbenchMessages.EditorManager_saveChangesQuestion, model.getName()); dialog = new MessageDialog(shellProvider.getShell(), WorkbenchMessages.Save_Resource, null, message, MessageDialog.QUESTION, buttons, 0) { protected int getShellStyle() { return (canCancel ? SWT.CLOSE : SWT.NONE) | SWT.TITLE | SWT.BORDER | SWT.APPLICATION_MODAL | getDefaultOrientation(); } }; } choice = SaveableHelper.testGetAutomatedResponse(); if (SaveableHelper.testGetAutomatedResponse() == SaveableHelper.USER_RESPONSE) { choice = dialog.open(); if(stillOpenElsewhere) { // map value of choice back to ISaveablePart2 values switch (choice) { case IDialogConstants.YES_ID: choice = ISaveablePart2.YES; break; case IDialogConstants.NO_ID: choice = ISaveablePart2.NO; break; case IDialogConstants.CANCEL_ID: choice = ISaveablePart2.CANCEL; break; default: break; } MessageDialogWithToggle dialogWithToggle = (MessageDialogWithToggle) dialog; if (choice != ISaveablePart2.CANCEL && dialogWithToggle.getToggleState()) { apiPreferenceStore.setValue(IWorkbenchPreferenceConstants.PROMPT_WHEN_SAVEABLE_STILL_OPEN, false); } } } // 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 modelsToSave.clear(); break; default: case ISaveablePart2.CANCEL: // cancel return true; } } else { MyListSelectionDialog dlg = new MyListSelectionDialog( shellProvider.getShell(), modelsToSave, new ArrayContentProvider(), new WorkbenchPartLabelProvider(), stillOpenElsewhere ? WorkbenchMessages.EditorManager_saveResourcesOptionallyMessage : WorkbenchMessages.EditorManager_saveResourcesMessage, canCancel, stillOpenElsewhere); dlg.setInitialSelections(modelsToSave.toArray()); dlg.setTitle(EditorManager.SAVE_RESOURCES_TITLE); // this "if" statement aids in testing. if (SaveableHelper.testGetAutomatedResponse() == SaveableHelper.USER_RESPONSE) { int result = dlg.open(); // Just return null to prevent the operation continuing if (result == IDialogConstants.CANCEL_ID) return true; if (dlg.getDontPromptSelection()) { apiPreferenceStore.setValue(IWorkbenchPreferenceConstants.PROMPT_WHEN_SAVEABLE_STILL_OPEN, false); } modelsToSave = Arrays.asList(dlg.getResult()); } } } // Create save block. return saveModels(modelsToSave, shellProvider, runnableContext); } /** * Save the given models. * @param finalModels the list of models to be saved * @param shellProvider the provider used to obtain a shell in prompting is * required. Clients can use a workbench window for this. * @param runnableContext a runnable context that will be used to provide a * progress monitor while the save is taking place. Clients can * use a workbench window for this. * @return <code>true</code> if the operation was canceled */ public boolean saveModels(final List finalModels, final IShellProvider shellProvider, IRunnableContext runnableContext) { IRunnableWithProgress progressOp = new IRunnableWithProgress() { public void run(IProgressMonitor monitor) { IProgressMonitor monitorWrap = new EventLoopProgressMonitor( monitor); monitorWrap.beginTask("", finalModels.size()); //$NON-NLS-1$ for (Iterator i = finalModels.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; } SaveableHelper.doSaveModel(model, new SubProgressMonitor(monitorWrap, 1), shellProvider, true); if (monitorWrap.isCanceled()) break; } monitorWrap.done(); } }; // Do the save. return !SaveableHelper.runProgressMonitorOperation( WorkbenchMessages.Save_All, progressOp, runnableContext, shellProvider); } private static class PostCloseInfo { private List partsClosing = new ArrayList(); private Map modelsDecrementing = new HashMap(); private Set modelsClosing = new HashSet(); } /** * @param postCloseInfoObject */ public void postClose(Object postCloseInfoObject) { PostCloseInfo postCloseInfo = (PostCloseInfo) postCloseInfoObject; List removed = new ArrayList(); for (Iterator it = postCloseInfo.partsClosing.iterator(); it.hasNext();) { IWorkbenchPart part = (IWorkbenchPart) it.next(); Set saveables = (Set) modelMap.get(part); if (saveables != null) { // make a copy to avoid a ConcurrentModificationException - we // will remove from the original set as we iterate saveables = new HashSet(saveables); for (Iterator it2 = saveables.iterator(); it2.hasNext();) { Saveable saveable = (Saveable) it2.next(); if (removeModel(part, saveable)) { removed.add(saveable); } } } } if (removed.size() > 0) { fireModelLifecycleEvent(new SaveablesLifecycleEvent(this, SaveablesLifecycleEvent.POST_CLOSE, (Saveable[]) removed .toArray(new Saveable[removed.size()]), false)); } } /** * Returns the saveable models provided by the given part. If the part does * not provide any models, a default model is returned representing the * part. * * @param part * the workbench part * @return the saveable models */ private Saveable[] getSaveables(IWorkbenchPart part) { if (part instanceof ISaveablesSource) { ISaveablesSource source = (ISaveablesSource) part; return source.getSaveables(); } else if (part instanceof ISaveablePart) { return new Saveable[] { new DefaultSaveable(part) }; } else { return new Saveable[0]; } } /** * @param actualPart */ public void postOpen(IWorkbenchPart part) { addModels(part, getSaveables(part)); } /** * @param actualPart */ public void dirtyChanged(IWorkbenchPart part) { Saveable[] saveables = getSaveables(part); if (saveables.length > 0) { fireModelLifecycleEvent(new SaveablesLifecycleEvent(this, SaveablesLifecycleEvent.DIRTY_CHANGED, saveables, false)); } } /** * For testing purposes. Not to be called by clients. * * @param model * @return */ public Object[] testGetSourcesForModel(Saveable model) { List result = new ArrayList(); for (Iterator it = modelMap.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); Set values = (Set) entry.getValue(); if (values.contains(model)) { result.add(entry.getKey()); } } return result.toArray(); } private static final class MyListSelectionDialog extends ListSelectionDialog { private final boolean canCancel; private Button checkbox; private boolean dontPromptSelection; private boolean stillOpenElsewhere; private MyListSelectionDialog(Shell shell, Object input, IStructuredContentProvider contentprovider, ILabelProvider labelProvider, String message, boolean canCancel, boolean stillOpenElsewhere) { super(shell, input, contentprovider, labelProvider, message); this.canCancel = canCancel; this.stillOpenElsewhere = stillOpenElsewhere; if (!canCancel) { int shellStyle = getShellStyle(); shellStyle &= ~SWT.CLOSE; setShellStyle(shellStyle); } } /** * @return */ public boolean getDontPromptSelection() { return dontPromptSelection; } protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); if (canCancel) { createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); } } protected Control createDialogArea(Composite parent) { Composite dialogAreaComposite = (Composite) super.createDialogArea(parent); if (stillOpenElsewhere) { Composite checkboxComposite = new Composite(dialogAreaComposite, SWT.NONE); checkboxComposite.setLayout(new GridLayout(2, false)); checkbox = new Button(checkboxComposite, SWT.CHECK); checkbox.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { dontPromptSelection = checkbox.getSelection(); } }); GridData gd = new GridData(); gd.horizontalAlignment = SWT.BEGINNING; checkbox.setLayoutData(gd); Label label = new Label(checkboxComposite, SWT.NONE); label.setText(WorkbenchMessages.EditorManager_closeWithoutPromptingOption); gd = new GridData(); gd.grabExcessHorizontalSpace = true; gd.horizontalAlignment = SWT.BEGINNING; } return dialogAreaComposite; } } /** * @return a list of ISaveablesSource objects registered with this saveables * list which are not workbench parts. */ public ISaveablesSource[] getNonPartSources() { return (ISaveablesSource[]) nonPartSources .toArray(new ISaveablesSource[nonPartSources.size()]); } /** * @param model */ public IWorkbenchPart[] getPartsForSaveable(Saveable model) { List result = new ArrayList(); for (Iterator it = modelMap.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); Set values = (Set) entry.getValue(); if (values.contains(model) && entry.getKey() instanceof IWorkbenchPart) { result.add(entry.getKey()); } } return (IWorkbenchPart[]) result.toArray(new IWorkbenchPart[result.size()]); } }