blob: 701083fc2c93b0efc3f121eb469c83d14d9e8c65 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006 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.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IModelLifecycleListener;
import org.eclipse.ui.ISaveableModel;
import org.eclipse.ui.ISaveableModelManager;
import org.eclipse.ui.ISaveableModelSource;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISaveablePart2;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.ModelLifecycleEvent;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ListSelectionDialog;
import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor;
import org.eclipse.ui.model.WorkbenchPartLabelProvider;
/**
* @since 3.2
*
*/
public class SaveableModelManager implements ISaveableModelManager {
private ListenerList listeners = new ListenerList();
// event source (mostly ISaveableModelSource) -> Set of ISaveableModel
private Map modelMap = new HashMap();
// reference counting map, ISaveableModel -> Integer
private Map modelRefCounts = new HashMap();
public ISaveableModel[] getOpenModels() {
return (ISaveableModel[]) modelRefCounts.keySet().toArray(
new ISaveableModel[modelRefCounts.size()]);
}
// returns true if this model has not yet been in getModels()
private boolean addModel(Object source, ISaveableModel model) {
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);
}
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, ISaveableModel model) {
boolean result = false;
Set modelsForSource = (Set) modelMap.get(source);
if (modelsForSource == null) {
modelsForSource = new HashSet();
modelMap.put(source, modelsForSource);
}
if (modelsForSource.remove(model)) {
result = decrementRefCount(modelRefCounts, model);
if (modelsForSource.isEmpty()) {
modelMap.remove(source);
}
}
return result;
}
public void handleModelLifecycleEvent(ModelLifecycleEvent event) {
ISaveableModel[] modelArray = event.getModels();
switch (event.getEventType()) {
case ModelLifecycleEvent.POST_OPEN:
addModels(event.getSource(), modelArray);
break;
case ModelLifecycleEvent.PRE_CLOSE:
ISaveableModel[] models = event.getModels();
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,
!event.isForce());
if (canceled) {
event.setVeto(true);
}
break;
case ModelLifecycleEvent.POST_CLOSE:
removeModels(event.getSource(), modelArray);
break;
case ModelLifecycleEvent.DIRTY_CHANGED:
fireModelLifecycleEvent(new ModelLifecycleEvent(this, event
.getEventType(), event.getModels(), false));
break;
}
}
/**
* @param source
* @param modelArray
*/
private void removeModels(Object source, ISaveableModel[] modelArray) {
List removed = new ArrayList();
for (int i = 0; i < modelArray.length; i++) {
ISaveableModel model = modelArray[i];
if (removeModel(source, model)) {
removed.add(model);
}
}
if (removed.size() > 0) {
fireModelLifecycleEvent(new ModelLifecycleEvent(this,
ModelLifecycleEvent.POST_OPEN, (ISaveableModel[]) removed
.toArray(new ISaveableModel[removed.size()]), false));
}
}
/**
* @param source
* @param modelArray
*/
private void addModels(Object source, ISaveableModel[] modelArray) {
List added = new ArrayList();
for (int i = 0; i < modelArray.length; i++) {
ISaveableModel model = modelArray[i];
if (addModel(source, model)) {
added.add(model);
}
}
if (added.size() > 0) {
fireModelLifecycleEvent(new ModelLifecycleEvent(this,
ModelLifecycleEvent.POST_OPEN, (ISaveableModel[]) added
.toArray(new ISaveableModel[added.size()]), false));
}
}
/**
* @param event
*/
private void fireModelLifecycleEvent(ModelLifecycleEvent event) {
Object[] listenerArray = listeners.getListeners();
for (int i = 0; i < listenerArray.length; i++) {
((IModelLifecycleListener) listenerArray[i])
.handleModelLifecycleEvent(event);
}
}
public void addModelLifecycleListener(IModelLifecycleListener listener) {
listeners.add(listener);
}
public void removeModelLifecycleListener(IModelLifecycleListener listener) {
listeners.remove(listener);
}
/**
* @param editorsToClose
* @param save
* @param window
* @return the post close info to be passed to postClose
*/
public Object preCloseParts(List editorsToClose, boolean save,
final IWorkbenchWindow window) {
// reference count (how many occurrences of a model will go away?)
PostCloseInfo postCloseInfo = new PostCloseInfo();
for (Iterator it = editorsToClose.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);
// only include this part in the following logic if it returned
// DEFAULT
if (response != ISaveablePart2.DEFAULT) {
continue;
}
}
ISaveableModel[] modelsFromSource = getSaveableModels(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, 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, boolean canCancel) {
// TODO prompt for saving of dirty modelsDecrementing but not closing
// (changes
// won't be lost)
List modelsToSave = new ArrayList();
for (Iterator it = modelsClosing.iterator(); it.hasNext();) {
ISaveableModel modelClosing = (ISaveableModel) it.next();
if (modelClosing.isDirty()) {
modelsToSave.add(modelClosing);
}
}
return modelsToSave.isEmpty() ? false : promptForSaving(modelsToSave,
window, canCancel);
}
/**
* @param modelsClosing
* @param modelsDecrementing
*/
private void fillModelsClosing(Set modelsClosing, Map modelsDecrementing) {
for (Iterator it = modelsDecrementing.keySet().iterator(); it.hasNext();) {
ISaveableModel model = (ISaveableModel) it.next();
if (modelsDecrementing.get(model).equals(modelRefCounts.get(model))) {
modelsClosing.add(model);
}
}
}
/**
* @param modelsToSave
* @param window
* @param canCancel
* @return true if the user canceled
*/
private boolean promptForSaving(List modelsToSave,
final IWorkbenchWindow window, boolean canCancel) {
// Save parts, exit the method if cancel is pressed.
if (modelsToSave.size() > 0) {
if (modelsToSave.size() == 1) {
ISaveableModel model = (ISaveableModel) modelsToSave.get(0);
String message = NLS.bind(
WorkbenchMessages.EditorManager_saveChangesQuestion,
model.getName());
// 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);
int choice = SaveableHelper.testGetAutomatedResponse();
if (SaveableHelper.testGetAutomatedResponse() == SaveableHelper.USER_RESPONSE) {
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
modelsToSave.clear();
break;
default:
case ISaveablePart2.CANCEL: // cancel
return true;
}
} else {
ListSelectionDialog dlg = new MyListSelectionDialog(window
.getShell(), modelsToSave, new ArrayContentProvider(),
new WorkbenchPartLabelProvider(),
EditorManager.RESOURCES_TO_SAVE_MESSAGE, canCancel);
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;
modelsToSave = Arrays.asList(dlg.getResult());
}
}
}
// Create save block.
final List finalModels = modelsToSave;
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();) {
ISaveableModel model = (ISaveableModel) i.next();
// handle case where this model got saved as a result of
// saving another
if (!model.isDirty()) {
monitor.worked(1);
continue;
}
try {
model.doSave(new SubProgressMonitor(monitorWrap, 1));
} catch (CoreException e) {
ErrorDialog.openError(window.getShell(),
WorkbenchMessages.Error, e.getMessage(), e
.getStatus());
}
if (monitorWrap.isCanceled())
break;
}
monitorWrap.done();
}
};
// Do the save.
if (!SaveableHelper.runProgressMonitorOperation(
WorkbenchMessages.Save_All, progressOp, window)) {
// cancelled
return true;
}
return false;
}
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();
ISaveableModel[] modelArray = getSaveableModels(part);
for (int i = 0; i < modelArray.length; i++) {
ISaveableModel model = modelArray[i];
if (removeModel(part, model)) {
removed.add(model);
}
}
}
if (removed.size() > 0) {
fireModelLifecycleEvent(new ModelLifecycleEvent(this,
ModelLifecycleEvent.POST_CLOSE, (ISaveableModel[]) removed
.toArray(new ISaveableModel[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 ISaveableModel[] getSaveableModels(IWorkbenchPart part) {
if (part instanceof ISaveableModelSource) {
ISaveableModelSource source = (ISaveableModelSource) part;
return source.getModels();
} else if (part instanceof ISaveablePart) {
return new ISaveableModel[] { new DefaultSaveableModel(part) };
} else {
return new ISaveableModel[0];
}
}
/**
* @param actualPart
*/
public void postOpen(IWorkbenchPart part) {
addModels(part, getSaveableModels(part));
}
/**
* @param actualPart
*/
public void dirtyChanged(IWorkbenchPart part) {
ISaveableModel[] saveableModels = getSaveableModels(part);
if (saveableModels.length > 0) {
fireModelLifecycleEvent(new ModelLifecycleEvent(this,
ModelLifecycleEvent.DIRTY_CHANGED, saveableModels, false));
}
}
/**
* For testing purposes. Not to be called by clients.
*
* @param model
* @return
*/
public Object[] testGetSourcesForModel(ISaveableModel 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 MyListSelectionDialog(Shell shell, Object input,
IStructuredContentProvider contentprovider,
ILabelProvider labelProvider, String message, boolean canCancel) {
super(shell, input, contentprovider, labelProvider, message);
this.canCancel = canCancel;
if (!canCancel) {
int shellStyle = getShellStyle();
shellStyle &= ~SWT.CLOSE;
setShellStyle(shellStyle);
}
}
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);
}
}
}
}