diff --git a/bundles/org.eclipse.e4.ui.progress/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.progress/META-INF/MANIFEST.MF index 56edde6f84e..5b86ba3891d 100644 --- a/bundles/org.eclipse.e4.ui.progress/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.e4.ui.progress/META-INF/MANIFEST.MF @@ -1,7 +1,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-SymbolicName: org.eclipse.e4.ui.progress;singleton:=true -Bundle-Version: 0.5.200.qualifier +Bundle-Version: 0.6.0.qualifier Bundle-Name: %pluginName Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/IProgressService.java b/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/IProgressService.java index 4c855db6d03..97a5a211678 100644 --- a/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/IProgressService.java +++ b/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/IProgressService.java @@ -141,6 +141,23 @@ public void runInUI(IRunnableContext context, public void busyCursorWhile(IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException; + /** + * Same as {@link #busyCursorWhile(IRunnableWithProgress)}, but the Cancel + * button of the progress dialog is disabled from the start if + * cancelable is false. User input is blocked in the + * same way in both cases, so prefer a cancelable operation or a + * {@link Job} for operations that may take long. + * + * @param runnable The runnable to execute and show the progress for. + * @param cancelable true if the user may cancel the operation, + * false if it can't be canceled + * @throws InvocationTargetException if executed operation failed + * @throws InterruptedException if operation is interrupted + * @since 0.6 + */ + public void busyCursorWhile(IRunnableWithProgress runnable, boolean cancelable) + throws InvocationTargetException, InterruptedException; + /** * This specialization of IRunnableContext#run(boolean, boolean, * IRunnableWithProgress) might run the runnable asynchronously diff --git a/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/internal/ProgressServiceImpl.java b/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/internal/ProgressServiceImpl.java index ee3e456d967..5a20f35f8cb 100644 --- a/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/internal/ProgressServiceImpl.java +++ b/bundles/org.eclipse.e4.ui.progress/src/org/eclipse/e4/ui/progress/internal/ProgressServiceImpl.java @@ -122,6 +122,12 @@ public Image getIconFor(Job job) { @Override public void busyCursorWhile(final IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException { + busyCursorWhile(runnable, true); + } + + @Override + public void busyCursorWhile(final IRunnableWithProgress runnable, final boolean cancelable) + throws InvocationTargetException, InterruptedException { final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog( ProgressManagerUtil.getDefaultParent(), this, progressManager, contentProviderFactory, finishedJobs); @@ -133,7 +139,7 @@ public void busyCursorWhile(final IRunnableWithProgress runnable) try { dialog.setOpenOnRun(false); setUserInterfaceActive(false); - dialog.run(true, true, runnable); + dialog.run(true, cancelable, runnable); } catch (InvocationTargetException e1) { invokes[0] = e1; } catch (InterruptedException e2) { @@ -257,13 +263,19 @@ public IStatus getStatus() { private void busyCursorWhile(Runnable dialogWaitRunnable, ProgressMonitorJobsDialog dialog) { // create the job that will open the dialog after a delay - scheduleProgressMonitorJob(dialog); - final Display display = getDisplay(); - if (display == null) { - return; + Job showDialogJob = scheduleProgressMonitorJob(dialog); + try { + final Display display = getDisplay(); + if (display == null) { + return; + } + // show a busy cursor until the dialog opens + BusyIndicator.showWhile(display, dialogWaitRunnable); + } finally { + // In case the dialog hasn't popped up yet, cancel it so it doesn't pop up after + // the operation finishes or unwinds with an exception. + showDialogJob.cancel(); } - // show a busy cursor until the dialog opens - BusyIndicator.showWhile(display, dialogWaitRunnable); } /** @@ -271,8 +283,9 @@ private void busyCursorWhile(Runnable dialogWaitRunnable, * * @param dialog * the dialog to open + * @return the job that was scheduled to open the dialog */ - private void scheduleProgressMonitorJob( + private Job scheduleProgressMonitorJob( final ProgressMonitorJobsDialog dialog) { Job updateJob = UIJob.create(ProgressMessages.ProgressManager_openJobName, monitor -> { @@ -283,7 +296,7 @@ private void scheduleProgressMonitorJob( }); updateJob.setSystem(true); updateJob.schedule(getLongOperationTime()); - + return updateJob; } /** diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java index 0116708ad6b..e5d767fb52a 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java @@ -919,6 +919,12 @@ ImageData[] getImageData(URL fileSystemPath, ImageLoader loader) { @Override public void busyCursorWhile(final IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException { + busyCursorWhile(runnable, true); + } + + @Override + public void busyCursorWhile(final IRunnableWithProgress runnable, final boolean cancelable) + throws InvocationTargetException, InterruptedException { final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog(ProgressManagerUtil.getDefaultParent()); dialog.setOpenOnRun(false); final InvocationTargetException[] invokes = new InvocationTargetException[1]; @@ -928,7 +934,7 @@ public void busyCursorWhile(final IRunnableWithProgress runnable) try { dialog.setOpenOnRun(false); setUserInterfaceActive(false); - dialog.run(true, true, runnable); + dialog.run(true, cancelable, runnable); } catch (InvocationTargetException e1) { invokes[0] = e1; } catch (InterruptedException e2) { diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/WorkbenchSiteProgressService.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/WorkbenchSiteProgressService.java index 5b4a263a140..ef8688675d8 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/WorkbenchSiteProgressService.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/WorkbenchSiteProgressService.java @@ -154,6 +154,12 @@ public void busyCursorWhile(IRunnableWithProgress runnable) throws InvocationTar getWorkbenchProgressService().busyCursorWhile(runnable); } + @Override + public void busyCursorWhile(IRunnableWithProgress runnable, boolean cancelable) + throws InvocationTargetException, InterruptedException { + getWorkbenchProgressService().busyCursorWhile(runnable, cancelable); + } + @Override public void schedule(Job job, long delay, boolean useHalfBusyCursor) { job.addJobChangeListener(getJobChangeListener(useHalfBusyCursor)); diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/progress/IProgressService.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/progress/IProgressService.java index 32e12d7c908..30c42860e4d 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/progress/IProgressService.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/progress/IProgressService.java @@ -139,6 +139,23 @@ void runInUI(IRunnableContext context, IRunnableWithProgress runnable, IScheduli */ void busyCursorWhile(IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException; + /** + * Same as {@link #busyCursorWhile(IRunnableWithProgress)}, but the Cancel + * button of the progress dialog is disabled from the start if + * cancelable is false. User input is blocked in the + * same way in both cases, so prefer a cancelable operation or a + * {@link Job} for operations that may take long. + * + * @param runnable The runnable to execute and show the progress for. + * @param cancelable true if the user may cancel the operation, + * false if it can't be canceled + * @throws InvocationTargetException if executed operation failed + * @throws InterruptedException if operation is interrupted + * @since 3.141 + */ + void busyCursorWhile(IRunnableWithProgress runnable, boolean cancelable) + throws InvocationTargetException, InterruptedException; + /** * This specialization of IRunnableContext#run(boolean, boolean, * IRunnableWithProgress) might run the runnable asynchronously if diff --git a/examples/org.eclipse.e4.ui.examples.job/META-INF/MANIFEST.MF b/examples/org.eclipse.e4.ui.examples.job/META-INF/MANIFEST.MF index 63bb21a3ce8..e38132c3e7b 100644 --- a/examples/org.eclipse.e4.ui.examples.job/META-INF/MANIFEST.MF +++ b/examples/org.eclipse.e4.ui.examples.job/META-INF/MANIFEST.MF @@ -9,7 +9,7 @@ Export-Package: org.eclipse.e4.ui.examples.jobs, Require-Bundle: org.eclipse.core.resources, org.eclipse.core.runtime, org.eclipse.jface.text, - org.eclipse.e4.ui.progress;bundle-version="0.1.0", + org.eclipse.e4.ui.progress;bundle-version="0.6.0", org.eclipse.swt;bundle-version="3.133.0", org.eclipse.e4.ui.di, org.eclipse.jface;bundle-version="3.10.0" diff --git a/examples/org.eclipse.e4.ui.examples.job/src/org/eclipse/e4/ui/examples/jobs/views/JobsView.java b/examples/org.eclipse.e4.ui.examples.job/src/org/eclipse/e4/ui/examples/jobs/views/JobsView.java index e1c77ef57f7..37a8ee2cd73 100644 --- a/examples/org.eclipse.e4.ui.examples.job/src/org/eclipse/e4/ui/examples/jobs/views/JobsView.java +++ b/examples/org.eclipse.e4.ui.examples.job/src/org/eclipse/e4/ui/examples/jobs/views/JobsView.java @@ -62,6 +62,7 @@ public class JobsView { private Text quantityField, delayField, rescheduleDelay; private Button schedulingRuleField; private Button noPromptField; + private Button cancelableField; Composite parent; @@ -79,13 +80,14 @@ protected void busyCursorWhile() { try { final long duration = getDuration(); final boolean shouldLock = lockField.getSelection(); + final boolean cancelable = cancelableField.getSelection(); progressService.busyCursorWhile( monitor -> { if (shouldLock) doRunInWorkspace(duration, monitor); else doRun(duration, monitor); - }); + }, cancelable); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { @@ -201,7 +203,13 @@ public void widgetSelected(SelectionEvent e) { }); // busy cursor while - Button busyWhile = new Button(group, SWT.PUSH); + Composite busyWhileRow = new Composite(group, SWT.NONE); + GridLayout busyWhileLayout = new GridLayout(2, false); + busyWhileLayout.marginWidth = 0; + busyWhileLayout.marginHeight = 0; + busyWhileRow.setLayout(busyWhileLayout); + busyWhileRow.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + Button busyWhile = new Button(busyWhileRow, SWT.PUSH); busyWhile.setText("busyCursorWhile"); //$NON-NLS-1$ busyWhile.setToolTipText("Uses IProgressService.busyCursorWhile"); //$NON-NLS-1$ busyWhile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); @@ -211,6 +219,10 @@ public void widgetSelected(SelectionEvent e) { busyCursorWhile(); } }); + cancelableField = new Button(busyWhileRow, SWT.CHECK); + cancelableField.setText("cancelable"); //$NON-NLS-1$ + cancelableField.setToolTipText("Whether the Cancel button of busyCursorWhile is enabled"); //$NON-NLS-1$ + cancelableField.setSelection(true); // progress monitor dialog with fork=false Button noFork = new Button(group, SWT.PUSH); noFork.setText("runInUI"); //$NON-NLS-1$ diff --git a/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/JobsView.java b/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/JobsView.java index 5be6880b0ad..7d9f536e11b 100644 --- a/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/JobsView.java +++ b/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/JobsView.java @@ -59,18 +59,20 @@ public class JobsView extends ViewPart { private Text quantityField, delayField, rescheduleDelay; private Button schedulingRuleField; private Button noPromptField; + private Button cancelableField; protected void busyCursorWhile() { try { final long duration = getDuration(); final boolean shouldLock = lockField.getSelection(); + final boolean cancelable = cancelableField.getSelection(); PlatformUI.getWorkbench().getProgressService().busyCursorWhile( monitor -> { if (shouldLock) doRunInWorkspace(duration, monitor); else doRun(duration, monitor); - }); + }, cancelable); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { @@ -188,11 +190,21 @@ private void createPushButtonGroup(Composite parent) { touch.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); touch.addSelectionListener(SelectionListener.widgetSelectedAdapter(_ -> touchWorkspace())); // busy cursor while - Button busyWhile = new Button(group, SWT.PUSH); + Composite busyWhileRow = new Composite(group, SWT.NONE); + GridLayout busyWhileLayout = new GridLayout(2, false); + busyWhileLayout.marginWidth = 0; + busyWhileLayout.marginHeight = 0; + busyWhileRow.setLayout(busyWhileLayout); + busyWhileRow.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + Button busyWhile = new Button(busyWhileRow, SWT.PUSH); busyWhile.setText("busyCursorWhile"); //$NON-NLS-1$ busyWhile.setToolTipText("Uses IProgressService.busyCursorWhile"); //$NON-NLS-1$ busyWhile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); busyWhile.addSelectionListener(SelectionListener.widgetSelectedAdapter(_ -> busyCursorWhile())); + cancelableField = new Button(busyWhileRow, SWT.CHECK); + cancelableField.setText("cancelable"); //$NON-NLS-1$ + cancelableField.setToolTipText("Whether the Cancel button of busyCursorWhile is enabled"); //$NON-NLS-1$ + cancelableField.setSelection(true); // progress monitor dialog with fork=false Button noFork = new Button(group, SWT.PUSH); noFork.setText("runInUI"); //$NON-NLS-1$ diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressServiceTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressServiceTest.java index c25e2979ded..f7826ad855d 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressServiceTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressServiceTest.java @@ -3,16 +3,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; @@ -367,6 +374,123 @@ private void assertDialogShown(boolean fork, boolean cancelable, boolean runInBa expectDialogShown, fork, cancelable, runInBackgroundPref, pumpUiEventsWhileRunning)); } + @Test + public void testBusyCursorWhile_cancelButtonIsEnabled() throws Exception { + assertTrue(isCancelButtonEnabledInBusyCursorDialog(r -> progressService.busyCursorWhile(r))); + } + + @Test + public void testBusyCursorWhile_cancelable_cancelButtonIsEnabled() throws Exception { + assertTrue(isCancelButtonEnabledInBusyCursorDialog(r -> progressService.busyCursorWhile(r, true))); + } + + @Test + public void testBusyCursorWhile_notCancelable_cancelButtonIsDisabled() throws Exception { + assertFalse(isCancelButtonEnabledInBusyCursorDialog(r -> progressService.busyCursorWhile(r, false))); + } + + /** + * Runs the given {@code busyCursorWhile} call with a runnable that waits for + * the progress dialog to open and returns whether the Cancel button of that + * dialog was enabled. + */ + private boolean isCancelButtonEnabledInBusyCursorDialog(BusyCursorCall call) throws Exception { + Display display = PlatformUI.getWorkbench().getDisplay(); + Set preexistingShells = new HashSet<>(Arrays.asList(display.getShells())); + AtomicReference cancelEnabled = new AtomicReference<>(); + call.run(monitor -> { + // The dialog opens after getLongOperationTime(), the UI thread is pumping + // events + long deadline = System.currentTimeMillis() + progressService.getLongOperationTime() * 10L; + while (cancelEnabled.get() == null && System.currentTimeMillis() < deadline) { + display.syncExec(() -> { + for (Shell shell : display.getShells()) { + if (!shell.isDisposed() && shell.isVisible() && !preexistingShells.contains(shell)) { + Button cancel = findCancelButton(shell); + if (cancel != null) { + cancelEnabled.set(cancel.isEnabled()); + } + } + } + }); + Thread.sleep(20); + } + }); + assertNotNull(cancelEnabled.get(), "The progress dialog with a Cancel button was not shown"); + return cancelEnabled.get(); + } + + @Test + public void testBusyCursorWhile_closingDialogIsIgnored() throws Exception { + assertClosingDialogIsIgnored(closeBusyCursorDialog(r -> progressService.busyCursorWhile(r))); + } + + @Test + public void testBusyCursorWhile_cancelable_closingDialogIsIgnored() throws Exception { + assertClosingDialogIsIgnored(closeBusyCursorDialog(r -> progressService.busyCursorWhile(r, true))); + } + + @Test + public void testBusyCursorWhile_notCancelable_closingDialogIsIgnored() throws Exception { + assertClosingDialogIsIgnored(closeBusyCursorDialog(r -> progressService.busyCursorWhile(r, false))); + } + + private static void assertClosingDialogIsIgnored(CloseResult result) { + assertFalse(result.canceled(), "Closing the dialog must not cancel the operation"); + assertFalse(result.closed(), "The dialog must stay open while the operation runs"); + } + + private record CloseResult(boolean canceled, boolean closed) { + } + + /** + * Waits for the progress dialog of the given {@code busyCursorWhile} call, + * closes its shell like the close box does and returns what happened. + */ + private CloseResult closeBusyCursorDialog(BusyCursorCall call) throws Exception { + Display display = PlatformUI.getWorkbench().getDisplay(); + Set preexistingShells = new HashSet<>(Arrays.asList(display.getShells())); + AtomicReference result = new AtomicReference<>(); + call.run(monitor -> { + long deadline = System.currentTimeMillis() + progressService.getLongOperationTime() * 10L; + while (result.get() == null && System.currentTimeMillis() < deadline) { + display.syncExec(() -> { + for (Shell shell : display.getShells()) { + if (!shell.isDisposed() && shell.isVisible() && !preexistingShells.contains(shell) + && findCancelButton(shell) != null) { + shell.close(); + boolean closed = shell.isDisposed() || !shell.isVisible(); + result.set(new CloseResult(monitor.isCanceled(), closed)); + } + } + }); + Thread.sleep(20); + } + }); + assertNotNull(result.get(), "The progress dialog with a Cancel button was not shown"); + return result.get(); + } + + private static Button findCancelButton(Composite parent) { + for (Control child : parent.getChildren()) { + if (child instanceof Button button + && Integer.valueOf(IDialogConstants.CANCEL_ID).equals(button.getData())) { + return button; + } + if (child instanceof Composite composite) { + Button found = findCancelButton(composite); + if (found != null) { + return found; + } + } + } + return null; + } + + interface BusyCursorCall { + void run(IRunnableWithProgress runnable) throws Exception; + } + private static void runInBackground(boolean value) { WorkbenchPlugin.getDefault().getPreferenceStore().setValue(IPreferenceConstants.RUN_IN_BACKGROUND, value); }