/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.actions;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.instrumentation.UIInstrumentation;
import com.google.dart.tools.ui.instrumentation.UIInstrumentationBuilder;
import com.google.dart.tools.ui.internal.text.editor.EditorUtility;
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.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
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.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.SelectionListenerAction;
import org.eclipse.ui.ide.IDE;
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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Standard action for deleting the currently selected resources.
* <p>
* NOTE: based on {@link org.eclipse.ui.actions.DeleteResourceAction} and modified to improve
* confirmation flow.
* <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.
*/
@SuppressWarnings("restriction")
public class DeleteResourceAction extends SelectionListenerAction {
static class DeleteProjectDialog extends MessageDialog {
static String getMessage(IResource[] projects) {
if (projects.length == 1) {
IProject project = (IProject) projects[0];
return NLS.bind(ActionMessages.DeleteResourceAction_confirmProject1, project.getName());
}
return NLS.bind(ActionMessages.DeleteResourceAction_confirmProjectN, new Integer(
projects.length));
}
static String getTitle(IResource[] projects) {
if (projects.length == 1) {
return ActionMessages.DeleteResourceAction_titleProject1;
}
return ActionMessages.DeleteResourceAction_titleProjectN;
}
DeleteProjectDialog(Shell parentShell, IResource[] projects) {
super(parentShell, getTitle(projects), null, /* default window icon */
getMessage(projects), MessageDialog.QUESTION, new String[] {
IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL}, 1 /* no is default */);
setShellStyle(getShellStyle() | SWT.SHEET);
}
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
PlatformUI.getWorkbench().getHelpSystem().setHelp(
newShell,
IIDEHelpContextIds.DELETE_PROJECT_DIALOG);
}
}
/**
* The id of this action.
*/
public static final String ID = PlatformUI.PLUGIN_ID + ".DeleteResourceAction";//$NON-NLS-1$
private IShellProvider shellProvider = null;
/**
* Whether or not we are deleting content for projects.
*/
private boolean deleteContent = false;
private String[] modelProviderIds;
/**
* Creates a new delete resource action.
*
* @param provider the shell provider to use. Must not be <code>null</code>.
*/
public DeleteResourceAction(IShellProvider provider) {
super(IDEWorkbenchMessages.DeleteResourceAction_text);
Assert.isNotNull(provider);
initAction();
setShellProvider(provider);
}
/**
* 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.
*/
public String[] getModelProviderIds() {
return modelProviderIds;
}
@Override
public void run() {
UIInstrumentationBuilder instrumentation = UIInstrumentation.builder("DeleteResourceAction.runClick");
try {
final IResource[] resources = getSelectedResourcesArray();
instrumentation.record(resources);
// Cache paths now since they can't be retrieved post delete
final IPath[] resourcePaths = new IPath[resources.length];
for (int i = 0; i < resources.length; i++) {
resourcePaths[i] = resources[i].getLocation();
}
// on Windows platform call out to system to do the delete, since
// Eclipse has no knowledge of junctions used in packages
if (DartCore.isWindows()) {
if (confirmDelete(resources)) {
windowsDelete(resources);
removeFromIgnores(resourcePaths);
}
instrumentation.metric("windows-delete", "true");
return;
}
if (LTKLauncher.openDeleteWizard(getStructuredSelection())) {
EditorUtility.closeOrphanedEditors();
instrumentation.metric("DeleteWizardShown", "true");
removeFromIgnores(resourcePaths);
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)) {
instrumentation.metric("Confirmed", "false");
return;
}
Job deletionCheckJob = new InstrumentedJob(
IDEWorkbenchMessages.DeleteResourceAction_checkJobName) {
@Override
public boolean belongsTo(Object family) {
if (IDEWorkbenchMessages.DeleteResourceAction_jobName.equals(family)) {
return true;
}
return super.belongsTo(family);
}
@Override
protected IStatus doRun(IProgressMonitor monitor, UIInstrumentationBuilder instrumentation) {
if (resources.length == 0) {
instrumentation.metric("Problem", "Resources.length == 0");
return Status.CANCEL_STATUS;
}
instrumentation.metric("resources-length", resources.length);
scheduleDeleteJob(resources);
removeFromIgnores(resourcePaths);
return Status.OK_STATUS;
}
};
deletionCheckJob.schedule();
} finally {
instrumentation.log();
}
}
/**
* 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.
*/
public void setModelProviderIds(String[] modelProviderIds) {
this.modelProviderIds = modelProviderIds;
}
/**
* 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(getSelectedResourcesArray());
}
/**
* 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(IResource[] resources) {
// 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;
}
if (resources.length == 0) {
return false;
}
// Return true if everything in the selection exists.
for (int i = 0; i < resources.length; i++) {
IResource resource = resources[i];
if (resource.isPhantom()) {
return false;
}
}
return true;
}
/**
* 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(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(IResource[] resources) {
String title;
String msg;
if (resources.length == 1) {
title = IDEWorkbenchMessages.DeleteResourceAction_title1;
IResource resource = resources[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,
new Integer(resources.length));
} else {
msg = NLS.bind(IDEWorkbenchMessages.DeleteResourceAction_confirmN, new Integer(
resources.length));
}
}
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(IResource[] resources) {
DeleteProjectDialog dialog = new DeleteProjectDialog(shellProvider.getShell(), resources);
int code = dialog.open();
deleteContent = code == 0 /* YES */;
return deleteContent;
}
/**
* 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(IResource[] resources) {
for (int i = 0; i < resources.length; i++) {
IResource resource = resources[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(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(IResource[] resources) {
int types = getSelectedResourceTypes(resources);
// note that the selection may contain multiple types of resource
return types == IResource.PROJECT;
}
/**
* Return an array of the currently selected resources.
*
* @return the selected resources
*/
private IResource[] getSelectedResourcesArray() {
List<?> selection = getSelectedResources();
IResource[] resources = new IResource[selection.size()];
selection.toArray(resources);
return resources;
}
/**
* Returns a bit-mask containing the types of resources in the selection.
*
* @param resources the selected resources
*/
private int getSelectedResourceTypes(IResource[] resources) {
int types = 0;
for (int i = 0; i < resources.length; i++) {
types |= resources[i].getType();
}
return types;
}
/**
* Action initialization.
*/
private void initAction() {
setToolTipText(IDEWorkbenchMessages.DeleteResourceAction_toolTip);
PlatformUI.getWorkbench().getHelpSystem().setHelp(
this,
IIDEHelpContextIds.DELETE_RESOURCE_ACTION);
setId(ID);
}
private void removeFromIgnores(IPath[] resourcePaths) {
if (resourcePaths != null) {
for (IPath path : resourcePaths) {
try {
DartCore.removeFromIgnores(path);
} catch (IOException e) {
DartToolsPlugin.log(e);
}
}
}
}
/**
* Schedule a job to delete the resources to delete.
*
* @param resourcesToDelete
*/
private void scheduleDeleteJob(final 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 boolean belongsTo(Object family) {
if (IDEWorkbenchMessages.DeleteResourceAction_jobName.equals(family)) {
return true;
}
return super.belongsTo(family);
}
@Override
public IStatus run(final IProgressMonitor monitor) {
try {
final DeleteResourcesOperation op = new DeleteResourcesOperation(
resourcesToDelete,
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 monitor) {
return op.computeExecutionStatus(monitor);
}
};
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);
}
}
};
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
IDE.saveAllEditors(resourcesToDelete, false);
}
});
deleteJob.setUser(true);
deleteJob.schedule();
}
private void setShellProvider(IShellProvider provider) {
shellProvider = provider;
}
/**
* Method is called for all deletes on Windows platform, since Eclipse does not have support for
* Junctions which are used by Pub to create symlinks.
*/
private void windowsDelete(IResource[] resources) {
List<IResource> resourceList = Arrays.asList(resources);
IProject project = null;
List<IResource> folders = new ArrayList<IResource>();
List<IResource> files = new ArrayList<IResource>();
for (IResource resource : resources) {
if (!resourceList.contains(resource.getParent())) {
if (resource.getType() == IResource.FILE) {
files.add(resource);
project = resource.getProject();
} else {
folders.add(resource);
if (!(resource instanceof IProject)) {
project = resource.getProject();
}
}
}
}
List<String> commandsList = new ArrayList<String>();
try {
if (!files.isEmpty()) {
commandsList.add("cmd");
commandsList.add("/C");
commandsList.add("del");
commandsList.add("/q");
for (IResource resource : files) {
commandsList.add(resource.getLocation().toOSString());
}
ProcessBuilder builder = new ProcessBuilder(commandsList);
Process process = builder.start();
process.waitFor();
}
if (!folders.isEmpty()) {
commandsList.clear();
commandsList.add("cmd");
commandsList.add("/C");
commandsList.add("rmdir");
commandsList.add("/s");
commandsList.add("/q");
for (IResource resource : folders) {
commandsList.add(resource.getLocation().toOSString());
if (resource instanceof IProject) {
((IProject) resource).delete(false, true, new NullProgressMonitor());
}
}
ProcessBuilder builder = new ProcessBuilder(commandsList);
Process process = builder.start();
process.waitFor();
}
if (project != null) {
project.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
}
EditorUtility.closeOrphanedEditors();
} catch (Exception e) {
DartToolsPlugin.log(e);
}
}
}