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 6aba4aedc63..7eeaf01440d 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
@@ -1075,22 +1075,28 @@ public void showInDialog(Shell shell, Job job) {
@Override
public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable)
throws InvocationTargetException, InterruptedException {
+ if (shouldRunInBackground()) {
+ // The "Always run in background" preference must suppress the modal
+ // progress dialog for every fork/cancelable combination, not just the
+ // backward compatible (fork==false || cancelable==false) path below.
+ final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog(
+ ProgressManagerUtil.getDefaultParent());
+ dialog.setOpenOnRun(false);
+ dialog.run(fork, cancelable, runnable);
+ return;
+ }
+
if (!fork || !cancelable) {
// Backward compatible code.
final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog(
ProgressManagerUtil.getDefaultParent());
- if (shouldRunInBackground()) {
- dialog.setOpenOnRun(false);
+ Job showDialogJob = scheduleProgressMonitorJob(dialog);
+ try {
dialog.run(fork, cancelable, runnable);
- } else {
- Job showDialogJob = scheduleProgressMonitorJob(dialog);
- try {
- dialog.run(fork, cancelable, runnable);
- } 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();
- }
+ } 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();
}
return;
}
diff --git a/examples/org.eclipse.ui.examples.job/plugin.xml b/examples/org.eclipse.ui.examples.job/plugin.xml
index 229c50aaafc..17f8a142720 100644
--- a/examples/org.eclipse.ui.examples.job/plugin.xml
+++ b/examples/org.eclipse.ui.examples.job/plugin.xml
@@ -22,6 +22,13 @@
class="org.eclipse.ui.examples.jobs.views.JobsView"
id="org.eclipse.ui.examples.jobs.views.JobsView">
+
+
diff --git a/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/ProgressServiceView.java b/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/ProgressServiceView.java
new file mode 100644
index 00000000000..9b3b52d4dc5
--- /dev/null
+++ b/examples/org.eclipse.ui.examples.job/src/org/eclipse/ui/examples/jobs/views/ProgressServiceView.java
@@ -0,0 +1,289 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Eclipse contributors and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.ui.examples.jobs.views;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.swt.SWT;
+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.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.internal.IPreferenceConstants;
+import org.eclipse.ui.part.ViewPart;
+import org.eclipse.ui.progress.IProgressService;
+
+/**
+ * A view that demonstrates (and lets you reproduce) the documented and
+ * undocumented behaviors of
+ * {@link IProgressService#run(boolean, boolean, IRunnableWithProgress)}
+ * across every combination of the fork/cancelable
+ * parameters, the "Always run in background" preference, and calling from a
+ * non-UI thread.
+ */
+@SuppressWarnings("restriction")
+public class ProgressServiceView extends ViewPart {
+
+ private static final int SLEEP_STEP_MS = 100;
+
+ private final ExecutorService nonUiExecutor = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r, "ProgressServiceView-nonUI"); //$NON-NLS-1$
+ t.setDaemon(true);
+ return t;
+ });
+
+ private Button backgroundPrefField;
+ private IPropertyChangeListener prefListener;
+
+ private Button forkField;
+ private Button cancelableField;
+ private Text durationField;
+
+ @Override
+ public void createPartControl(Composite parent) {
+ Composite body = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 1;
+ body.setLayout(layout);
+ body.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createPreferenceGroup(body);
+ createRunGroup(body);
+ createForkIllustrationGroup(body);
+ createNonUiGroup(body);
+ }
+
+ private void createPreferenceGroup(Composite parent) {
+ Composite group = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 1;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ backgroundPrefField = new Button(group, SWT.CHECK);
+ backgroundPrefField.setText("Always run in background"); //$NON-NLS-1$
+ backgroundPrefField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ backgroundPrefField.setSelection(getPreferenceStore().getBoolean(IPreferenceConstants.RUN_IN_BACKGROUND));
+ backgroundPrefField.addSelectionListener(SelectionListener.widgetSelectedAdapter(
+ e -> getPreferenceStore().setValue(IPreferenceConstants.RUN_IN_BACKGROUND,
+ backgroundPrefField.getSelection())));
+
+ Label hint = new Label(group, SWT.WRAP);
+ hint.setText("This preference can also be set under Preferences > General."); //$NON-NLS-1$
+ hint.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ prefListener = event -> {
+ if (IPreferenceConstants.RUN_IN_BACKGROUND.equals(event.getProperty())) {
+ if (backgroundPrefField.isDisposed()) {
+ return;
+ }
+ Display display = backgroundPrefField.getDisplay();
+ if (display.isDisposed()) {
+ return;
+ }
+ display.asyncExec(() -> {
+ if (!backgroundPrefField.isDisposed()) {
+ backgroundPrefField
+ .setSelection(getPreferenceStore().getBoolean(IPreferenceConstants.RUN_IN_BACKGROUND));
+ }
+ });
+ }
+ };
+ getPreferenceStore().addPropertyChangeListener(prefListener);
+ }
+
+ private void createRunGroup(Composite parent) {
+ Composite group = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Label label = new Label(group, SWT.NONE);
+ label.setText("Duration (ms):"); //$NON-NLS-1$
+ durationField = new Text(group, SWT.BORDER);
+ durationField.setText("3000"); //$NON-NLS-1$
+ durationField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ forkField = new Button(group, SWT.CHECK);
+ forkField.setText("fork"); //$NON-NLS-1$
+ forkField.setSelection(true);
+ GridData forkData = new GridData(GridData.FILL_HORIZONTAL);
+ forkData.horizontalSpan = 2;
+ forkField.setLayoutData(forkData);
+
+ cancelableField = new Button(group, SWT.CHECK);
+ cancelableField.setText("cancelable"); //$NON-NLS-1$
+ cancelableField.setSelection(true);
+ GridData cancelableData = new GridData(GridData.FILL_HORIZONTAL);
+ cancelableData.horizontalSpan = 2;
+ cancelableField.setLayoutData(cancelableData);
+
+ Button run = new Button(group, SWT.PUSH);
+ run.setText("Run via IProgressService.run(fork, cancelable, ...)"); //$NON-NLS-1$
+ GridData runData = new GridData(GridData.FILL_HORIZONTAL);
+ runData.horizontalSpan = 2;
+ run.setLayoutData(runData);
+ run.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> runViaProgressService()));
+ }
+
+ private void createForkIllustrationGroup(Composite parent) {
+ Composite group = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 1;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Button naive = new Button(group, SWT.PUSH);
+ naive.setText("fork=false (naive - will freeze)"); //$NON-NLS-1$
+ naive.setToolTipText("Calls run(false, true, ...) with a plain sleep loop. Watch the heartbeat stop."); //$NON-NLS-1$
+ naive.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ naive.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> runForkFalseNaive()));
+
+ Button pumping = new Button(group, SWT.PUSH);
+ pumping.setText("fork=false (pumping events - correct)"); //$NON-NLS-1$
+ pumping.setToolTipText(
+ "Calls run(false, true, ...) with a loop that calls Display.readAndDispatch(). The heartbeat keeps ticking."); //$NON-NLS-1$
+ pumping.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ pumping.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> runForkFalsePumping()));
+ }
+
+ private void createNonUiGroup(Composite parent) {
+ Composite group = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 1;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Button nonUiButton = new Button(group, SWT.PUSH);
+ nonUiButton.setText("Run from non-UI thread (expect exception)"); //$NON-NLS-1$
+ nonUiButton.setToolTipText(
+ "IProgressService.run() must be called from the UI thread; this documents the resulting exception."); //$NON-NLS-1$
+ nonUiButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ nonUiButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> runFromNonUIThread()));
+ }
+
+ @SuppressWarnings("deprecation")
+ private IPreferenceStore getPreferenceStore() {
+ return PlatformUI.getWorkbench().getPreferenceStore();
+ }
+
+ private long getDuration() {
+ try {
+ return Long.parseLong(durationField.getText().trim());
+ } catch (NumberFormatException e) {
+ Platform.getLog(ProgressServiceView.class).error(e.getMessage(), e);
+ return 3000;
+ }
+ }
+
+ private IRunnableWithProgress createSleepRunnable(long durationMillis, boolean pumpEvents) {
+ return monitor -> {
+ int ticks = (int) Math.max(1, durationMillis / SLEEP_STEP_MS);
+ monitor.beginTask("Simulated long-running operation", ticks); //$NON-NLS-1$
+ try {
+ for (int i = 0; i < ticks; i++) {
+ if (monitor.isCanceled()) {
+ return;
+ }
+ try {
+ Thread.sleep(SLEEP_STEP_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ if (pumpEvents) {
+ Display display = Display.getCurrent();
+ if (display != null) {
+ while (display.readAndDispatch()) {
+ // drain pending UI events so the heartbeat keeps ticking
+ }
+ }
+ }
+ monitor.worked(1);
+ }
+ } finally {
+ monitor.done();
+ }
+ };
+ }
+
+ private void runViaProgressService() {
+ boolean fork = forkField.getSelection();
+ boolean cancelable = cancelableField.getSelection();
+ long duration = getDuration();
+ IProgressService service = PlatformUI.getWorkbench().getProgressService();
+ try {
+ service.run(fork, cancelable, createSleepRunnable(duration, false));
+ } catch (InvocationTargetException | InterruptedException e) {
+ Platform.getLog(ProgressServiceView.class).error(e.getMessage(), e);
+ }
+ }
+
+ private void runForkFalseNaive() {
+ long duration = getDuration();
+ boolean cancelable = cancelableField.getSelection();
+ try {
+ PlatformUI.getWorkbench().getProgressService().run(false, cancelable, createSleepRunnable(duration, false));
+ } catch (InvocationTargetException | InterruptedException e) {
+ Platform.getLog(ProgressServiceView.class).error(e.getMessage(), e);
+ }
+ }
+
+ private void runForkFalsePumping() {
+ long duration = getDuration();
+ boolean cancelable = cancelableField.getSelection();
+ try {
+ PlatformUI.getWorkbench().getProgressService().run(false, cancelable, createSleepRunnable(duration, true));
+ } catch (InvocationTargetException | InterruptedException e) {
+ Platform.getLog(ProgressServiceView.class).error(e.getMessage(), e);
+ }
+ }
+
+ private void runFromNonUIThread() {
+ boolean fork = forkField.getSelection();
+ boolean cancelable = cancelableField.getSelection();
+ long duration = getDuration();
+ nonUiExecutor.execute(() -> {
+ try {
+ PlatformUI.getWorkbench().getProgressService().run(fork, cancelable,
+ createSleepRunnable(duration, false));
+ } catch (Throwable t) {
+ Platform.getLog(ProgressServiceView.class).error(t.getMessage(), t);
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ if (prefListener != null) {
+ getPreferenceStore().removePropertyChangeListener(prefListener);
+ }
+ nonUiExecutor.shutdownNow();
+ super.dispose();
+ }
+
+ @Override
+ public void setFocus() {
+ // do nothing
+ }
+
+}
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
new file mode 100644
index 00000000000..a29475f087b
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressServiceTest.java
@@ -0,0 +1,161 @@
+package org.eclipse.ui.tests.progress;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.internal.IPreferenceConstants;
+import org.eclipse.ui.internal.WorkbenchPlugin;
+import org.eclipse.ui.internal.progress.FinishedJobs;
+import org.eclipse.ui.progress.IProgressService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @since 3.5
+ *
+ */
+public class ProgressServiceTest extends ProgressTestCase {
+
+ private IProgressService progressService;
+
+ @Override
+ @Before
+ public void doSetUp() throws Exception {
+ window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ progressService = PlatformUI.getWorkbench().getProgressService();
+ FinishedJobs.getInstance().clearAll();
+ }
+
+ @Override
+ @After
+ public void doTearDown() throws Exception {
+ FinishedJobs.getInstance().clearAll();
+ WorkbenchPlugin.getDefault().getPreferenceStore().setToDefault(IPreferenceConstants.RUN_IN_BACKGROUND);
+ super.doTearDown();
+ }
+
+ @Test
+ public void testRun_fork_cancelable_runInBackground_noDialogShown() throws Exception {
+ assertDialogShown(true, true, true, false);
+ }
+
+ @Test
+ public void testRun_fork_cancelable_runInForeground_dialogShown() throws Exception {
+ assertDialogShown(true, true, false, true);
+ }
+
+ @Test
+ public void testRun_fork_notCancelable_runInBackground_noDialogShown() throws Exception {
+ assertDialogShown(true, false, true, false);
+ }
+
+ @Test
+ public void testRun_fork_notCancelable_runInForeground_dialogShown() throws Exception {
+ assertDialogShown(true, false, false, true);
+ }
+
+ @Test
+ public void testRun_noFork_cancelable_runInBackground_noDialogShown() throws Exception {
+ assertDialogShown(false, true, true, false);
+ }
+
+ @Test
+ public void testRun_noFork_cancelable_runInForeground_dialogShown() throws Exception {
+ assertDialogShown(false, true, false, true);
+ }
+
+ @Test
+ public void testRun_noFork_notCancelable_runInBackground_noDialogShown() throws Exception {
+ assertDialogShown(false, false, true, false);
+ }
+
+ @Test
+ public void testRun_noFork_notCancelable_runInForeground_dialogShown() throws Exception {
+ assertDialogShown(false, false, false, true);
+ }
+
+ /**
+ * Runs {@link IProgressService#run(boolean, boolean, IRunnableWithProgress)}
+ * with the given {@code fork}/{@code cancelable} arguments and the given
+ * "Always run in background" preference, detects whether the progress dialog
+ * popped up while it was running, and asserts that against
+ * {@code expectDialogShown}.
+ */
+ private void assertDialogShown(boolean fork, boolean cancelable, boolean runInBackgroundPref,
+ boolean expectDialogShown) throws Exception {
+ runInBackground(runInBackgroundPref);
+
+ boolean dialogShown = runAndDetectProgressDialog(() -> progressService.run(fork, cancelable, monitor -> {
+ try {
+ int workUnits = 3;
+ // Give it enough time so that the progress dialog can appear
+ long workDurationMillis = (long) (progressService.getLongOperationTime() / workUnits * 1.5);
+ SubMonitor sub = SubMonitor.convert(monitor, workUnits);
+ for (int i = 0; i < workUnits; i++) {
+ Thread.sleep(workDurationMillis);
+ sub.worked(1);
+ }
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }));
+
+ assertEquals(expectDialogShown, dialogShown,
+ String.format("Expected progress dialog shown=%s for fork=%s, cancelable=%s, runInBackground=%s",
+ expectDialogShown, fork, cancelable, runInBackgroundPref));
+ }
+
+ /**
+ * Runs {@code action} while watching for any newly shown {@link Shell}
+ * (i.e. one that did not already exist right before {@code action} started)
+ * becoming visible on the display. Uses an {@link SWT#Show} display filter
+ * rather than polling, because {@code Shell.setVisible(true)} (called by
+ * {@code Window.open()}) fires that event synchronously as a direct consequence
+ * of the call - it does not require the SWT event loop to be pumped. This
+ * matters because with {@code fork == false} the calling (UI) thread never gets
+ * to pump events again once the runnable starts (it just freezes), so a
+ * poll-based approach would never see a dialog that was already opened
+ * synchronously right before the freeze.
+ */
+ private boolean runAndDetectProgressDialog(ThrowingRunnable action) throws Exception {
+ Display display = PlatformUI.getWorkbench().getDisplay();
+ Set preexistingShells = new HashSet<>(Arrays.asList(display.getShells()));
+ AtomicBoolean dialogShown = new AtomicBoolean(false);
+ Listener showListener = event -> {
+ if (event.widget instanceof Shell shell && !preexistingShells.contains(shell)) {
+ dialogShown.set(true);
+ }
+ };
+
+ // SWT fires Show synchronously as part of Shell.setVisible(true) itself so
+ // it will fire even during the frozen, non-pumping case (i.e. fork == false).
+ display.addFilter(SWT.Show, showListener);
+ try {
+ action.run();
+ } finally {
+ display.removeFilter(SWT.Show, showListener);
+ }
+ return dialogShown.get();
+ }
+
+ static void runInBackground(boolean value) {
+ WorkbenchPlugin.getDefault().getPreferenceStore().setValue(IPreferenceConstants.RUN_IN_BACKGROUND, value);
+ }
+
+ interface ThrowingRunnable {
+ void run() throws Exception;
+ }
+
+}