From 376b326a52d7557c5cc7bf6025fc170c80080ef9 Mon Sep 17 00:00:00 2001 From: Federico Jeanne Date: Mon, 19 Oct 2026 16:48:11 +0200 Subject: [PATCH 1/2] Add IProgressService.busyCursorWhile(runnable, cancelable) busyCursorWhile always showed an enabled Cancel button. The new overload disables it from the start for operations that can't be canceled while keeping the input blocking of busyCursorWhile. Add "cancelable" option to the JobsView examples Cancel delayed progress dialog after busy cursor run (e4), same as it was done in b95f72783a for the ProgressManager. Co-Authored-By: Claude Opus 5.5 --- .../e4/ui/progress/IProgressService.java | 17 +++ .../internal/ProgressServiceImpl.java | 31 +++-- .../ui/internal/progress/ProgressManager.java | 8 +- .../WorkbenchSiteProgressService.java | 6 + .../eclipse/ui/progress/IProgressService.java | 17 +++ .../META-INF/MANIFEST.MF | 2 +- .../e4/ui/examples/jobs/views/JobsView.java | 16 ++- .../ui/examples/jobs/views/JobsView.java | 16 ++- .../tests/progress/ProgressServiceTest.java | 124 ++++++++++++++++++ 9 files changed, 222 insertions(+), 15 deletions(-) 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); } From 1ea502bdcf343e59bb3a20e94d0df6c95b51ca95 Mon Sep 17 00:00:00 2001 From: Federico Jeanne Date: Tue, 20 Oct 2026 08:33:27 +0200 Subject: [PATCH 2/2] Version bump --- bundles/org.eclipse.e4.ui.progress/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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