001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: * Benjamin Muskalla <b.muskalla@gmx.net>
011: * - Fix for bug 172574 - [IDE] DeleteProjectDialog inconsequent selection behavior
012:
013: *******************************************************************************/package org.eclipse.ui.actions;
014:
015: import java.util.List;
016:
017: import org.eclipse.core.commands.ExecutionException;
018: import org.eclipse.core.resources.IProject;
019: import org.eclipse.core.resources.IResource;
020: import org.eclipse.core.runtime.CoreException;
021: import org.eclipse.core.runtime.IProgressMonitor;
022: import org.eclipse.core.runtime.IStatus;
023: import org.eclipse.core.runtime.Status;
024: import org.eclipse.core.runtime.jobs.Job;
025: import org.eclipse.jface.dialogs.Dialog;
026: import org.eclipse.jface.dialogs.IDialogConstants;
027: import org.eclipse.jface.dialogs.MessageDialog;
028: import org.eclipse.jface.viewers.IStructuredSelection;
029: import org.eclipse.jface.window.Window;
030: import org.eclipse.osgi.util.NLS;
031: import org.eclipse.swt.SWT;
032: import org.eclipse.swt.events.MouseAdapter;
033: import org.eclipse.swt.events.MouseEvent;
034: import org.eclipse.swt.events.SelectionAdapter;
035: import org.eclipse.swt.events.SelectionEvent;
036: import org.eclipse.swt.events.SelectionListener;
037: import org.eclipse.swt.graphics.FontMetrics;
038: import org.eclipse.swt.graphics.GC;
039: import org.eclipse.swt.layout.GridData;
040: import org.eclipse.swt.layout.GridLayout;
041: import org.eclipse.swt.widgets.Button;
042: import org.eclipse.swt.widgets.Composite;
043: import org.eclipse.swt.widgets.Control;
044: import org.eclipse.swt.widgets.Label;
045: import org.eclipse.swt.widgets.Shell;
046: import org.eclipse.ui.PlatformUI;
047: import org.eclipse.ui.ide.undo.DeleteResourcesOperation;
048: import org.eclipse.ui.ide.undo.WorkspaceUndoUtil;
049: import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
050: import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
051: import org.eclipse.ui.internal.ide.IIDEHelpContextIds;
052: import org.eclipse.ui.progress.WorkbenchJob;
053:
054: /**
055: * Standard action for deleting the currently selected resources.
056: * <p>
057: * This class may be instantiated; it is not intended to be subclassed.
058: * </p>
059: */
060: public class DeleteResourceAction extends SelectionListenerAction {
061:
062: static class DeleteProjectDialog extends MessageDialog {
063:
064: private IResource[] projects;
065:
066: private boolean deleteContent = false;
067:
068: /**
069: * Control testing mode. In testing mode, it returns true to delete
070: * contents and does not pop up the dialog.
071: */
072: private boolean fIsTesting = false;
073:
074: private Button radio1;
075:
076: private Button radio2;
077:
078: DeleteProjectDialog(Shell parentShell, IResource[] projects) {
079: super (parentShell,
080: getTitle(projects),
081: null, // accept the
082: // default window
083: // icon
084: getMessage(projects), MessageDialog.QUESTION,
085: new String[] { IDialogConstants.YES_LABEL,
086: IDialogConstants.NO_LABEL }, 0); // yes is the
087: // default
088: this .projects = projects;
089: }
090:
091: static String getTitle(IResource[] projects) {
092: if (projects.length == 1) {
093: return IDEWorkbenchMessages.DeleteResourceAction_titleProject1;
094: }
095: return IDEWorkbenchMessages.DeleteResourceAction_titleProjectN;
096: }
097:
098: static String getMessage(IResource[] projects) {
099: if (projects.length == 1) {
100: IProject project = (IProject) projects[0];
101: return NLS
102: .bind(
103: IDEWorkbenchMessages.DeleteResourceAction_confirmProject1,
104: project.getName());
105: }
106: return NLS
107: .bind(
108: IDEWorkbenchMessages.DeleteResourceAction_confirmProjectN,
109: new Integer(projects.length));
110: }
111:
112: /*
113: * (non-Javadoc) Method declared on Window.
114: */
115: protected void configureShell(Shell newShell) {
116: super .configureShell(newShell);
117: PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell,
118: IIDEHelpContextIds.DELETE_PROJECT_DIALOG);
119: }
120:
121: protected Control createCustomArea(Composite parent) {
122: Composite composite = new Composite(parent, SWT.NONE);
123: composite.setLayout(new GridLayout());
124: radio1 = new Button(composite, SWT.RADIO);
125: radio1.addSelectionListener(selectionListener);
126: String text1;
127: if (projects.length == 1) {
128: IProject project = (IProject) projects[0];
129: if (project == null || project.getLocation() == null) {
130: text1 = IDEWorkbenchMessages.DeleteResourceAction_deleteContentsN;
131: } else {
132: text1 = NLS
133: .bind(
134: IDEWorkbenchMessages.DeleteResourceAction_deleteContents1,
135: project.getLocation().toOSString());
136: }
137: } else {
138: text1 = IDEWorkbenchMessages.DeleteResourceAction_deleteContentsN;
139: }
140: radio1.setText(text1);
141: radio1.setFont(parent.getFont());
142:
143: // Add explanatory label that the action cannot be undone.
144: // We can't put multi-line formatted text in a radio button,
145: // so we have to create a separate label.
146: Label detailsLabel = new Label(composite, SWT.LEFT);
147: detailsLabel
148: .setText(IDEWorkbenchMessages.DeleteResourceAction_deleteContentsDetails);
149: detailsLabel.setFont(parent.getFont());
150: // indent the explanatory label
151: GC gc = new GC(detailsLabel);
152: gc.setFont(detailsLabel.getParent().getFont());
153: FontMetrics fontMetrics = gc.getFontMetrics();
154: gc.dispose();
155: GridData data = new GridData();
156: data.horizontalIndent = Dialog
157: .convertHorizontalDLUsToPixels(fontMetrics,
158: IDialogConstants.INDENT);
159: detailsLabel.setLayoutData(data);
160: // add a listener so that clicking on the label selects the
161: // corresponding radio button.
162: // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=172574
163: detailsLabel.addMouseListener(new MouseAdapter() {
164: public void mouseUp(MouseEvent e) {
165: deleteContent = true;
166: radio1.setSelection(deleteContent);
167: radio2.setSelection(!deleteContent);
168: }
169: });
170: // Add a spacer label
171: new Label(composite, SWT.LEFT);
172:
173: radio2 = new Button(composite, SWT.RADIO);
174: radio2.addSelectionListener(selectionListener);
175: String text2 = IDEWorkbenchMessages.DeleteResourceAction_doNotDeleteContents;
176: radio2.setText(text2);
177: radio2.setFont(parent.getFont());
178:
179: // set initial state
180: radio1.setSelection(deleteContent);
181: radio2.setSelection(!deleteContent);
182:
183: return composite;
184: }
185:
186: private SelectionListener selectionListener = new SelectionAdapter() {
187: public void widgetSelected(SelectionEvent e) {
188: Button button = (Button) e.widget;
189: if (button.getSelection()) {
190: deleteContent = (button == radio1);
191: }
192: }
193: };
194:
195: boolean getDeleteContent() {
196: return deleteContent;
197: }
198:
199: /*
200: * (non-Javadoc)
201: *
202: * @see org.eclipse.jface.window.Window#open()
203: */
204: public int open() {
205: // Override Window#open() to allow for non-interactive testing.
206: if (fIsTesting) {
207: deleteContent = true;
208: return Window.OK;
209: }
210: return super .open();
211: }
212:
213: /**
214: * Set this delete dialog into testing mode. It won't pop up, and it
215: * returns true for deleteContent.
216: *
217: * @param t
218: * the testing mode
219: */
220: void setTestingMode(boolean t) {
221: fIsTesting = t;
222: }
223: }
224:
225: /**
226: * The id of this action.
227: */
228: public static final String ID = PlatformUI.PLUGIN_ID
229: + ".DeleteResourceAction";//$NON-NLS-1$
230:
231: /**
232: * The shell in which to show any dialogs.
233: */
234: private Shell shell;
235:
236: /**
237: * Whether or not we are deleting content for projects.
238: */
239: private boolean deleteContent = false;
240:
241: /**
242: * Flag that allows testing mode ... it won't pop up the project delete
243: * dialog, and will return "delete all content".
244: */
245: protected boolean fTestingMode = false;
246:
247: private String[] modelProviderIds;
248:
249: /**
250: * Creates a new delete resource action.
251: *
252: * @param shell
253: * the shell for any dialogs
254: */
255: public DeleteResourceAction(Shell shell) {
256: super (IDEWorkbenchMessages.DeleteResourceAction_text);
257: setToolTipText(IDEWorkbenchMessages.DeleteResourceAction_toolTip);
258: PlatformUI.getWorkbench().getHelpSystem().setHelp(this ,
259: IIDEHelpContextIds.DELETE_RESOURCE_ACTION);
260: setId(ID);
261: if (shell == null) {
262: throw new IllegalArgumentException();
263: }
264: this .shell = shell;
265: }
266:
267: /**
268: * Returns whether delete can be performed on the current selection.
269: *
270: * @param resources
271: * the selected resources
272: * @return <code>true</code> if the resources can be deleted, and
273: * <code>false</code> if the selection contains non-resources or
274: * phantom resources
275: */
276: private boolean canDelete(IResource[] resources) {
277: // allow only projects or only non-projects to be selected;
278: // note that the selection may contain multiple types of resource
279: if (!(containsOnlyProjects(resources) || containsOnlyNonProjects(resources))) {
280: return false;
281: }
282:
283: if (resources.length == 0) {
284: return false;
285: }
286: // Return true if everything in the selection exists.
287: for (int i = 0; i < resources.length; i++) {
288: IResource resource = resources[i];
289: if (resource.isPhantom()) {
290: return false;
291: }
292: }
293: return true;
294: }
295:
296: /**
297: * Returns whether the selection contains linked resources.
298: *
299: * @param resources
300: * the selected resources
301: * @return <code>true</code> if the resources contain linked resources,
302: * and <code>false</code> otherwise
303: */
304: private boolean containsLinkedResource(IResource[] resources) {
305: for (int i = 0; i < resources.length; i++) {
306: IResource resource = resources[i];
307: if (resource.isLinked()) {
308: return true;
309: }
310: }
311: return false;
312: }
313:
314: /**
315: * Returns whether the selection contains only non-projects.
316: *
317: * @param resources
318: * the selected resources
319: * @return <code>true</code> if the resources contains only non-projects,
320: * and <code>false</code> otherwise
321: */
322: private boolean containsOnlyNonProjects(IResource[] resources) {
323: int types = getSelectedResourceTypes(resources);
324: // check for empty selection
325: if (types == 0) {
326: return false;
327: }
328: // note that the selection may contain multiple types of resource
329: return (types & IResource.PROJECT) == 0;
330: }
331:
332: /**
333: * Returns whether the selection contains only projects.
334: *
335: * @param resources
336: * the selected resources
337: * @return <code>true</code> if the resources contains only projects, and
338: * <code>false</code> otherwise
339: */
340: private boolean containsOnlyProjects(IResource[] resources) {
341: int types = getSelectedResourceTypes(resources);
342: // note that the selection may contain multiple types of resource
343: return types == IResource.PROJECT;
344: }
345:
346: /**
347: * Asks the user to confirm a delete operation.
348: *
349: * @param resources
350: * the selected resources
351: * @return <code>true</code> if the user says to go ahead, and
352: * <code>false</code> if the deletion should be abandoned
353: */
354: private boolean confirmDelete(IResource[] resources) {
355: if (containsOnlyProjects(resources)) {
356: return confirmDeleteProjects(resources);
357: }
358: return confirmDeleteNonProjects(resources);
359:
360: }
361:
362: /**
363: * Asks the user to confirm a delete operation, where the selection contains
364: * no projects.
365: *
366: * @param resources
367: * the selected resources
368: * @return <code>true</code> if the user says to go ahead, and
369: * <code>false</code> if the deletion should be abandoned
370: */
371: private boolean confirmDeleteNonProjects(IResource[] resources) {
372: String title;
373: String msg;
374: if (resources.length == 1) {
375: title = IDEWorkbenchMessages.DeleteResourceAction_title1;
376: IResource resource = resources[0];
377: if (resource.isLinked()) {
378: msg = NLS
379: .bind(
380: IDEWorkbenchMessages.DeleteResourceAction_confirmLinkedResource1,
381: resource.getName());
382: } else {
383: msg = NLS
384: .bind(
385: IDEWorkbenchMessages.DeleteResourceAction_confirm1,
386: resource.getName());
387: }
388: } else {
389: title = IDEWorkbenchMessages.DeleteResourceAction_titleN;
390: if (containsLinkedResource(resources)) {
391: msg = NLS
392: .bind(
393: IDEWorkbenchMessages.DeleteResourceAction_confirmLinkedResourceN,
394: new Integer(resources.length));
395: } else {
396: msg = NLS
397: .bind(
398: IDEWorkbenchMessages.DeleteResourceAction_confirmN,
399: new Integer(resources.length));
400: }
401: }
402: return MessageDialog.openQuestion(shell, title, msg);
403: }
404:
405: /**
406: * Asks the user to confirm a delete operation, where the selection contains
407: * only projects. Also remembers whether project content should be deleted.
408: *
409: * @param resources
410: * the selected resources
411: * @return <code>true</code> if the user says to go ahead, and
412: * <code>false</code> if the deletion should be abandoned
413: */
414: private boolean confirmDeleteProjects(IResource[] resources) {
415: DeleteProjectDialog dialog = new DeleteProjectDialog(shell,
416: resources);
417: dialog.setTestingMode(fTestingMode);
418: int code = dialog.open();
419: deleteContent = dialog.getDeleteContent();
420: return code == 0; // YES
421: }
422:
423: /**
424: * Return an array of the currently selected resources.
425: *
426: * @return the selected resources
427: */
428: private IResource[] getSelectedResourcesArray() {
429: List selection = getSelectedResources();
430: IResource[] resources = new IResource[selection.size()];
431: selection.toArray(resources);
432: return resources;
433: }
434:
435: /**
436: * Returns a bit-mask containing the types of resources in the selection.
437: *
438: * @param resources
439: * the selected resources
440: */
441: private int getSelectedResourceTypes(IResource[] resources) {
442: int types = 0;
443: for (int i = 0; i < resources.length; i++) {
444: types |= resources[i].getType();
445: }
446: return types;
447: }
448:
449: /*
450: * (non-Javadoc) Method declared on IAction.
451: */
452: public void run() {
453: final IResource[] resources = getSelectedResourcesArray();
454: // WARNING: do not query the selected resources more than once
455: // since the selection may change during the run,
456: // e.g. due to window activation when the prompt dialog is dismissed.
457: // For more details, see Bug 60606 [Navigator] (data loss) Navigator
458: // deletes/moves the wrong file
459: if (!confirmDelete(resources)) {
460: return;
461: }
462:
463: Job deletionCheckJob = new Job(
464: IDEWorkbenchMessages.DeleteResourceAction_checkJobName) {
465:
466: /*
467: * (non-Javadoc)
468: *
469: * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
470: */
471: protected IStatus run(IProgressMonitor monitor) {
472: if (resources.length == 0)
473: return Status.CANCEL_STATUS;
474: scheduleDeleteJob(resources);
475: return Status.OK_STATUS;
476: }
477:
478: /*
479: * (non-Javadoc)
480: *
481: * @see org.eclipse.core.runtime.jobs.Job#belongsTo(java.lang.Object)
482: */
483: public boolean belongsTo(Object family) {
484: if (IDEWorkbenchMessages.DeleteResourceAction_jobName
485: .equals(family)) {
486: return true;
487: }
488: return super .belongsTo(family);
489: }
490: };
491:
492: deletionCheckJob.schedule();
493:
494: }
495:
496: /**
497: * Schedule a job to delete the resources to delete.
498: *
499: * @param resourcesToDelete
500: */
501: private void scheduleDeleteJob(final IResource[] resourcesToDelete) {
502: // use a non-workspace job with a runnable inside so we can avoid
503: // periodic updates
504: Job deleteJob = new Job(
505: IDEWorkbenchMessages.DeleteResourceAction_jobName) {
506: public IStatus run(final IProgressMonitor monitor) {
507: try {
508: final DeleteResourcesOperation op = new DeleteResourcesOperation(
509: resourcesToDelete,
510: IDEWorkbenchMessages.DeleteResourceAction_operationLabel,
511: deleteContent);
512: op.setModelProviderIds(getModelProviderIds());
513: // If we are deleting projects and their content, do not
514: // execute the operation in the undo history, since it cannot be
515: // properly restored. Just execute it directly so it won't be
516: // added to the undo history.
517: if (deleteContent
518: && containsOnlyProjects(resourcesToDelete)) {
519: // We must compute the execution status first so that any user prompting
520: // or validation checking occurs. Do it in a syncExec because
521: // we are calling this from a Job.
522: WorkbenchJob statusJob = new WorkbenchJob(
523: "Status checking") { //$NON-NLS-1$
524: /* (non-Javadoc)
525: * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
526: */
527: public IStatus runInUIThread(
528: IProgressMonitor monitor) {
529: return op
530: .computeExecutionStatus(monitor);
531: }
532:
533: };
534:
535: statusJob.setSystem(true);
536: statusJob.schedule();
537: try {//block until the status is ready
538: statusJob.join();
539: } catch (InterruptedException e) {
540: //Do nothing as status will be a cancel
541: }
542:
543: if (statusJob.getResult().isOK()) {
544: return op.execute(monitor,
545: WorkspaceUndoUtil
546: .getUIInfoAdapter(shell));
547: }
548: return statusJob.getResult();
549: }
550: return PlatformUI.getWorkbench()
551: .getOperationSupport()
552: .getOperationHistory().execute(
553: op,
554: monitor,
555: WorkspaceUndoUtil
556: .getUIInfoAdapter(shell));
557: } catch (ExecutionException e) {
558: if (e.getCause() instanceof CoreException) {
559: return ((CoreException) e.getCause())
560: .getStatus();
561: }
562: return new Status(IStatus.ERROR,
563: IDEWorkbenchPlugin.IDE_WORKBENCH, e
564: .getMessage(), e);
565: }
566: }
567:
568: /*
569: * (non-Javadoc)
570: *
571: * @see org.eclipse.core.runtime.jobs.Job#belongsTo(java.lang.Object)
572: */
573: public boolean belongsTo(Object family) {
574: if (IDEWorkbenchMessages.DeleteResourceAction_jobName
575: .equals(family)) {
576: return true;
577: }
578: return super .belongsTo(family);
579: }
580:
581: };
582: deleteJob.setUser(true);
583: deleteJob.schedule();
584: }
585:
586: /**
587: * The <code>DeleteResourceAction</code> implementation of this
588: * <code>SelectionListenerAction</code> method disables the action if the
589: * selection contains phantom resources or non-resources
590: */
591: protected boolean updateSelection(IStructuredSelection selection) {
592: return super .updateSelection(selection)
593: && canDelete(getSelectedResourcesArray());
594: }
595:
596: /**
597: * Returns the model provider ids that are known to the client that
598: * instantiated this operation.
599: *
600: * @return the model provider ids that are known to the client that
601: * instantiated this operation.
602: * @since 3.2
603: */
604: public String[] getModelProviderIds() {
605: return modelProviderIds;
606: }
607:
608: /**
609: * Sets the model provider ids that are known to the client that
610: * instantiated this operation. Any potential side effects reported by these
611: * models during validation will be ignored.
612: *
613: * @param modelProviderIds
614: * the model providers known to the client who is using this
615: * operation.
616: * @since 3.2
617: */
618: public void setModelProviderIds(String[] modelProviderIds) {
619: this.modelProviderIds = modelProviderIds;
620: }
621: }
|