blob: 6bac533758e94327108b13369d8e1dfc618e6da2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2016 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
* Benjamin Muskalla <b.muskalla@gmx.net> - Bug 172574
* Andrey Loskutov <loskutov@gmx.de> - Bug 41431, 462760
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 472784
* Mickael Istria (Red Hat Inc.) - Bug 486901
*******************************************************************************/
package org.eclipse.ui.actions;
import java.util.List;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
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.PlatformUI;
import org.eclipse.ui.ide.undo.DeleteResourcesOperation;
import org.eclipse.ui.ide.undo.WorkspaceUndoUtil;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.internal.ide.IIDEHelpContextIds;
import org.eclipse.ui.internal.ide.actions.LTKLauncher;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* Standard action for deleting the currently selected resources.
* <p>
* This class may be instantiated; it is not intended to be subclassed.
* </p>
* @noextend This class is not intended to be subclassed by clients.
*/
public class DeleteResourceAction extends SelectionListenerAction {
static class DeleteProjectDialog extends MessageDialog {
private List<? extends IResource> projects;
private boolean deleteContent;
/**
* Control testing mode. In testing mode, it returns true to delete
* contents and does not pop up the dialog.
*/
private boolean fIsTesting;
private Button radio1;
private Button radio2;
DeleteProjectDialog(Shell parentShell, List<? extends IResource> projects) {
super(parentShell, getTitle(projects), null, // accept the
// default window
// icon
getMessage(projects), MessageDialog.QUESTION, 0,
IDialogConstants.YES_LABEL,
IDialogConstants.NO_LABEL);
this.projects = projects;
setShellStyle(getShellStyle() | SWT.SHEET);
}
static String getTitle(List<? extends IResource> projects) {
if (projects.size() == 1) {
return IDEWorkbenchMessages.DeleteResourceAction_titleProject1;
}
return IDEWorkbenchMessages.DeleteResourceAction_titleProjectN;
}
static String getMessage(List<? extends IResource> projects) {
if (projects.size() == 1) {
IProject project = (IProject) projects.get(0);
return NLS
.bind(
IDEWorkbenchMessages.DeleteResourceAction_confirmProject1,
project.getName());
}
return NLS.bind(
IDEWorkbenchMessages.DeleteResourceAction_confirmProjectN,
Integer.valueOf((projects.size())));
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell, IIDEHelpContextIds.DELETE_PROJECT_DIALOG);
}
@Override
protected Control createCustomArea(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout());
radio1 = new Button(composite, SWT.RADIO);
radio1.addSelectionListener(selectionListener);
String text1;
if (projects.size() == 1) {
IProject project = (IProject) projects.get(0);
if (project == null || project.getLocation() == null) {
text1 = IDEWorkbenchMessages.DeleteResourceAction_deleteContentsN;
} else {
text1 = NLS
.bind(
IDEWorkbenchMessages.DeleteResourceAction_deleteContents1,
project.getLocation().toOSString());
}
} else {
text1 = IDEWorkbenchMessages.DeleteResourceAction_deleteContentsN;
}
radio1.setText(text1);
radio1.setFont(parent.getFont());
// Add explanatory label that the action cannot be undone.
// We can't put multi-line formatted text in a radio button,
// so we have to create a separate label.
Label detailsLabel = new Label(composite, SWT.LEFT);
detailsLabel.setText(IDEWorkbenchMessages.DeleteResourceAction_deleteContentsDetails);
detailsLabel.setFont(parent.getFont());
// indent the explanatory label
GridData data = new GridData();
data.horizontalIndent = 20;
detailsLabel.setLayoutData(data);
// add a listener so that clicking on the label selects the
// corresponding radio button.
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=172574
detailsLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
deleteContent = true;
radio1.setSelection(deleteContent);
radio2.setSelection(!deleteContent);
}
});
// Add a spacer label
new Label(composite, SWT.LEFT);
radio2 = new Button(composite, SWT.RADIO);
radio2.addSelectionListener(selectionListener);
String text2 = IDEWorkbenchMessages.DeleteResourceAction_doNotDeleteContents;
radio2.setText(text2);
radio2.setFont(parent.getFont());
// set initial state
radio1.setSelection(deleteContent);
radio2.setSelection(!deleteContent);
return composite;
}
private SelectionListener selectionListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
Button button = (Button) e.widget;
if (button.getSelection()) {
deleteContent = (button == radio1);
}
}
};
boolean getDeleteContent() {
return deleteContent;
}
@Override
public int open() {
// Override Window#open() to allow for non-interactive testing.
if (fIsTesting) {
deleteContent = true;
return Window.OK;
}
return super.open();
}
/**
* Set this delete dialog into testing mode. It won't pop up, and it
* returns true for deleteContent.
*
* @param t
* the testing mode
*/
void setTestingMode(boolean t) {
fIsTesting = t;
}
}
/**
* The id of this action.
*/
public static final String ID = PlatformUI.PLUGIN_ID + ".DeleteResourceAction";//$NON-NLS-1$
private IShellProvider shellProvider;
/**
* Whether or not we are deleting content for projects.
*/
private boolean deleteContent;
/**
* Flag that allows testing mode ... it won't pop up the project delete
* dialog, and will return "delete all content".
*/
protected boolean fTestingMode;
private String[] modelProviderIds;
/**
* Creates a new delete resource action.
*
* @param shell
* the shell for any dialogs
* @deprecated Should take an IShellProvider, see
* {@link #DeleteResourceAction(IShellProvider)}
*/
@Deprecated
public DeleteResourceAction(final Shell shell) {
super(IDEWorkbenchMessages.DeleteResourceAction_text);
Assert.isNotNull(shell);
initAction();
setShellProvider(() -> shell);
}
/**
* Creates a new delete resource action.
*
* @param provider
* the shell provider to use. Must not be <code>null</code>.
* @since 3.4
*/
public DeleteResourceAction(IShellProvider provider) {
super(IDEWorkbenchMessages.DeleteResourceAction_text);
Assert.isNotNull(provider);
initAction();
setShellProvider(provider);
}
/**
* Action initialization.
*/
private void initAction() {
setToolTipText(IDEWorkbenchMessages.DeleteResourceAction_toolTip);
PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IIDEHelpContextIds.DELETE_RESOURCE_ACTION);
setId(ID);
}
private void setShellProvider(IShellProvider provider) {
shellProvider = provider;
}
/**
* Returns whether delete can be performed on the current selection.
*
* @param resources
* the selected resources
* @return <code>true</code> if the resources can be deleted, and
* <code>false</code> if the selection contains non-resources or
* phantom resources
*/
private boolean canDelete(List<? extends IResource> resources) {
if (resources.isEmpty()) {
return false;
}
// allow only projects or only non-projects to be selected;
// note that the selection may contain multiple types of resource
if (!(containsOnlyProjects(resources) || containsOnlyNonProjects(resources))) {
return false;
}
// Return false if at least one element is not existing or workspace
// root
for (IResource resource : resources) {
if (resource.isPhantom() || resource.getType() == IResource.ROOT) {
return false;
}
}
return true;
}
/**
* Returns whether the selection contains linked resources.
*
* @param resources
* the selected resources
* @return <code>true</code> if the resources contain linked resources,
* and <code>false</code> otherwise
*/
private boolean containsLinkedResource(List<? extends IResource> resources) {
for (int i = 0; i < resources.size(); i++) {
IResource resource = resources.get(i);
if (resource.isLinked()) {
return true;
}
}
return false;
}
/**
* Returns whether the selection contains only non-projects.
*
* @param resources
* the selected resources
* @return <code>true</code> if the resources contains only non-projects,
* and <code>false</code> otherwise
*/
private boolean containsOnlyNonProjects(List<? extends IResource> resources) {
int types = getSelectedResourceTypes(resources);
// check for empty selection
if (types == 0) {
return false;
}
// note that the selection may contain multiple types of resource
return (types & IResource.PROJECT) == 0;
}
/**
* Returns whether the selection contains only projects.
*
* @param resources
* the selected resources
* @return <code>true</code> if the resources contains only projects, and
* <code>false</code> otherwise
*/
private boolean containsOnlyProjects(List<? extends IResource> resources) {
int types = getSelectedResourceTypes(resources);
// note that the selection may contain multiple types of resource
return types == IResource.PROJECT;
}
/**
* Asks the user to confirm a delete operation.
*
* @param resources
* the selected resources
* @return <code>true</code> if the user says to go ahead, and
* <code>false</code> if the deletion should be abandoned
*/
private boolean confirmDelete(List<? extends IResource> resources) {
if (containsOnlyProjects(resources)) {
return confirmDeleteProjects(resources);
}
return confirmDeleteNonProjects(resources);
}
/**
* Asks the user to confirm a delete operation, where the selection contains
* no projects.
*
* @param resources
* the selected resources
* @return <code>true</code> if the user says to go ahead, and
* <code>false</code> if the deletion should be abandoned
*/
private boolean confirmDeleteNonProjects(List<? extends IResource> resources) {
String title;
String msg;
if (resources.size() == 1) {
title = IDEWorkbenchMessages.DeleteResourceAction_title1;
IResource resource = resources.get(0);
if (resource.isLinked()) {
msg = NLS
.bind(
IDEWorkbenchMessages.DeleteResourceAction_confirmLinkedResource1,
resource.getName());
} else {
msg = NLS.bind(
IDEWorkbenchMessages.DeleteResourceAction_confirm1,
resource.getName());
}
} else {
title = IDEWorkbenchMessages.DeleteResourceAction_titleN;
if (containsLinkedResource(resources)) {
msg = NLS.bind(IDEWorkbenchMessages.DeleteResourceAction_confirmLinkedResourceN, resources.size());
} else {
msg = NLS.bind(IDEWorkbenchMessages.DeleteResourceAction_confirmN, resources.size());
}
}
return MessageDialog.openQuestion(shellProvider.getShell(), title, msg);
}
/**
* Asks the user to confirm a delete operation, where the selection contains
* only projects. Also remembers whether project content should be deleted.
*
* @param resources
* the selected resources
* @return <code>true</code> if the user says to go ahead, and
* <code>false</code> if the deletion should be abandoned
*/
private boolean confirmDeleteProjects(List<? extends IResource> resources) {
DeleteProjectDialog dialog = new DeleteProjectDialog(shellProvider.getShell(), resources);
dialog.setTestingMode(fTestingMode);
int code = dialog.open();
deleteContent = dialog.getDeleteContent();
return code == 0; // YES
}
/**
* Returns a bit-mask containing the types of resources in the selection.
*
* @param resources
* the selected resources
*/
private int getSelectedResourceTypes(List<? extends IResource> resources) {
int types = 0;
for (int i = 0; i < resources.size(); i++) {
types |= resources.get(i).getType();
}
return types;
}
@Override
public void run() {
final List<? extends IResource> resources = getSelectedResources();
if (resources.isEmpty()) {
return;
}
if (!fTestingMode) {
if (LTKLauncher.openDeleteWizard(getStructuredSelection())) {
CloseResourceAction.closeMatchingEditors(resources, true);
return;
}
}
// WARNING: do not query the selected resources more than once
// since the selection may change during the run,
// e.g. due to window activation when the prompt dialog is dismissed.
// For more details, see Bug 60606 [Navigator] (data loss) Navigator
// deletes/moves the wrong file
if (!confirmDelete(resources)) {
return;
}
Job deletionCheckJob = new Job(IDEWorkbenchMessages.DeleteResourceAction_checkJobName) {
@Override
protected IStatus run(IProgressMonitor monitor) {
if (resources.isEmpty()) {
return Status.CANCEL_STATUS;
}
scheduleDeleteJob(resources);
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
if (IDEWorkbenchMessages.DeleteResourceAction_jobName.equals(family)) {
return true;
}
return super.belongsTo(family);
}
};
deletionCheckJob.schedule();
}
/**
* Schedule a job to delete the resources to delete.
*
* @param resourcesToDelete
*/
private void scheduleDeleteJob(final List<? extends IResource> resourcesToDelete) {
// use a non-workspace job with a runnable inside so we can avoid
// periodic updates
Job deleteJob = new Job(IDEWorkbenchMessages.DeleteResourceAction_jobName) {
@Override
public IStatus run(final IProgressMonitor monitor) {
try {
final DeleteResourcesOperation op =
new DeleteResourcesOperation(resourcesToDelete.toArray(new IResource[resourcesToDelete.size()]),
IDEWorkbenchMessages.DeleteResourceAction_operationLabel, deleteContent);
op.setModelProviderIds(getModelProviderIds());
// If we are deleting projects and their content, do not
// execute the operation in the undo history, since it cannot be
// properly restored. Just execute it directly so it won't be
// added to the undo history.
if (deleteContent && containsOnlyProjects(resourcesToDelete)) {
// We must compute the execution status first so that any user prompting
// or validation checking occurs. Do it in a syncExec because
// we are calling this from a Job.
WorkbenchJob statusJob = new WorkbenchJob("Status checking"){ //$NON-NLS-1$
@Override
public IStatus runInUIThread(IProgressMonitor m) {
return op.computeExecutionStatus(m);
}
};
statusJob.setSystem(true);
statusJob.schedule();
try {//block until the status is ready
statusJob.join();
} catch (InterruptedException e) {
//Do nothing as status will be a cancel
}
if (statusJob.getResult().isOK()) {
return op.execute(monitor, WorkspaceUndoUtil.getUIInfoAdapter(shellProvider.getShell()));
}
return statusJob.getResult();
}
return PlatformUI.getWorkbench().getOperationSupport()
.getOperationHistory().execute(op, monitor,
WorkspaceUndoUtil.getUIInfoAdapter(shellProvider.getShell()));
} catch (ExecutionException e) {
if (e.getCause() instanceof CoreException) {
return ((CoreException)e.getCause()).getStatus();
}
return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, e.getMessage(),e);
}
}
@Override
public boolean belongsTo(Object family) {
if (IDEWorkbenchMessages.DeleteResourceAction_jobName.equals(family)) {
return true;
}
return super.belongsTo(family);
}
};
deleteJob.setUser(true);
deleteJob.schedule();
}
/**
* The <code>DeleteResourceAction</code> implementation of this
* <code>SelectionListenerAction</code> method disables the action if the
* selection contains phantom resources or non-resources
*/
@Override
protected boolean updateSelection(IStructuredSelection selection) {
return super.updateSelection(selection) && canDelete(getSelectedResources());
}
/**
* Returns the model provider ids that are known to the client that
* instantiated this operation.
*
* @return the model provider ids that are known to the client that
* instantiated this operation.
* @since 3.2
*/
public String[] getModelProviderIds() {
return modelProviderIds;
}
/**
* Sets the model provider ids that are known to the client that
* instantiated this operation. Any potential side effects reported by these
* models during validation will be ignored.
*
* @param modelProviderIds
* the model providers known to the client who is using this
* operation.
* @since 3.2
*/
public void setModelProviderIds(String[] modelProviderIds) {
this.modelProviderIds = modelProviderIds;
}
}