Skip to content

Commit

Permalink
Add support to display the busy indicator while a Future is running
Browse files Browse the repository at this point in the history
While the methods to perform an operation with busy indication are
already a great help to write responsive UI there is one case missing
where a (Completable)Future is already present, this can for example be
used to cache some heavy work.

This now adds a new BusyIndicator.showWhile(Future) method that covers
the use case of an externally provided future computation so the caller
can be sure that while it takes place the UI shows an indicator.

Beside that the Snippet30 was enhanced to demonstrate the new function.
Apply language suggestions from code review

Co-authored-by: Ed Merks <[email protected]>
Co-authored-by: Sebastian Zarnekow <[email protected]>
  • Loading branch information
3 people committed Jun 23, 2024
1 parent bd74b49 commit 2d3ec43
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.cocoa.macosx.aarch64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.cocoa.macosx.x86_64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.gtk.linux.aarch64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.gtk.linux.loongarch64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.gtk.linux.ppc64le;singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.gtk.linux.x86_64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.win32.win32.aarch64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.126.100,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.win32.win32.x86_64; singleton:=true
Bundle-Version: 3.126.100.qualifier
Bundle-Version: 3.127.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,81 @@ public static void showWhile(Display display, Runnable runnable) {
}
}

/**
* If called from a {@link Display} thread, waits for the given
* <code>Future</code> to complete and provides busy feedback using the busy
* indicator. While waiting for completion, pending UI events are processed to
* prevent UI freeze.
*
* If there is no {@link Display} for the current thread, the
* {@link Future#get()} will be executed ignoring any {@link ExecutionException}
* and no busy feedback will be displayed.
*
* @param future the {@link Future} for which busy feedback is to be shown.
* @since 3.127
* @implNote In some cases completion is detected by a regular timed wakeup of
* the {@link Display} thread,for minimal latency pass a
* {@link CompletableFuture} or trigger {@link Display#wake()} as an
* external event.
*/
public static void showWhile(Future<?> future) {
if (future == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
}
if (!future.isDone()) {
Display display = Display.getCurrent();
if (display == null || display.isDisposed()) {
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
// ignore caller might want to handle this...
} catch (CancellationException e) {
// ignore but caller might want to check afterwards
}
} else {
Integer busyId = setBusyCursor(display);
try {
if (future instanceof CompletionStage<?> stage) {
// let us wake up from sleep once the future is done
stage.handle((nil1, nil2) -> {
if (!display.isDisposed()) {
try {
display.wake();
} catch (SWTException e) {
// ignore then, this can happen due to the async nature between our check for
// disposed and the actual call to wake the display can be disposed
}
}
return null;
});
} else {
// for plain features we need to use a workaround, we install a timer every
// few ms, that should be short enough to not be noticeable by the user and long
// enough to not burn more CPU time than necessary
int wakeTime = 10;
display.timerExec(wakeTime, new Runnable() {
@Override
public void run() {
if (!future.isDone() && !display.isDisposed()) {
display.timerExec(wakeTime, this);
}
}
});
}
while (!future.isDone() && !display.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
} finally {
clearBusyCursor(display, busyId);
}
}
}
}

/**
* If called from a {@link Display} thread use the given {@link SwtRunnable} to
* produces a {@link CompletableFuture} providing busy feedback using the busy
Expand Down Expand Up @@ -184,6 +259,9 @@ public static <V, E extends Exception> CompletableFuture<V> compute(SwtCallable<
}

private static void clearBusyCursor(Display display, Integer busyId) {
if (display.isDisposed()) {
return;
}
Shell[] shells = display.getShells();
for (Shell shell : shells) {
Integer id = (Integer) shell.getData(BUSYID_NAME);
Expand Down Expand Up @@ -218,4 +296,4 @@ private static void setCursorAndId(Shell shell, Cursor cursor, Integer busyId) {
shell.setData(BUSYID_NAME, busyId);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ public static void main(String[] args) {
AtomicInteger nextId = new AtomicInteger();
Button b = new Button(shell, SWT.PUSH);
b.setText("invoke long running job");
Button b2 = new Button(shell, SWT.PUSH);
b2.setText("wait for predefined future");
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
try {
TimeUnit.SECONDS.sleep(30);
System.out.println("Future is done!");
} catch (InterruptedException e) {
return "Task is interrupted";
}
return "Text computed in the background";
});
b.addSelectionListener(widgetSelectedAdapter(e -> {
int id = nextId.incrementAndGet();
text.append("\nStart long running task " + id);
Expand All @@ -61,6 +72,16 @@ public static void main(String[] args) {
}, display);

}));
b2.addSelectionListener(widgetSelectedAdapter(e -> {
text.append("\nWaiting for Background Work to complete...");
b2.setEnabled(false);
BusyIndicator.showWhile(future);
if (text.isDisposed() || b2.isDisposed()) {
return;
}
b2.setEnabled(true);
text.append("\nBackground work has completed");
}));
shell.setSize(500, 300);
shell.open();
while (!shell.isDisposed()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
Test_org_eclipse_swt_custom_StyledText_multiCaretsSelections.class,
Test_org_eclipse_swt_custom_StyledTextLineSpacingProvider.class,
Test_org_eclipse_swt_custom_CTabFolder.class, Test_org_eclipse_swt_widgets_Spinner.class,
Test_org_eclipse_swt_widgets_ScrolledComposite.class})
Test_org_eclipse_swt_widgets_ScrolledComposite.class,
Test_org_eclipse_swt_custom_BusyIndicator.class})
public class AllWidgetTests {
public static void main(String[] args) {
JUnitCore.main(AllWidgetTests.class.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*******************************************************************************
* Copyright (c) 2024 Christoph Läubrich 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
*
* Contributors:
* Christoph Läubrich - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.tests.junit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.junit.Test;

public class Test_org_eclipse_swt_custom_BusyIndicator {

@Test
public void testShowWhile() {
Shell shell = new Shell();
Display display = shell.getDisplay();
Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT);
CountDownLatch latch = new CountDownLatch(1);
CompletableFuture<?> future = CompletableFuture.runAsync(() -> {
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
});

CountDownLatch latchNested = new CountDownLatch(1);
CompletableFuture<?> futureNested = CompletableFuture.runAsync(() -> {
try {
latchNested.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
});

assertNotEquals(busyCursor, shell.getCursor());

// This it proves that events on the display are executed
display.asyncExec(() -> {
// This will happen during the showWhile(future) from below.
BusyIndicator.showWhile(futureNested);
});

Cursor[] cursorInAsync = new Cursor[2];

// this serves two purpose:
// 1) it proves that events on the display are executed
// 2) it checks that the shell has the busy cursor during the nest showWhile.
display.asyncExec(() -> {
cursorInAsync[0] = shell.getCursor();
latchNested.countDown();
});

// this serves two purpose:
// 1) it proves that events on the display are executed
// 2) it checks that the shell has the busy cursor even after the termination of
// the nested showWhile.
display.asyncExec(() -> {
cursorInAsync[1] = shell.getCursor();
latch.countDown();
});

BusyIndicator.showWhile(future);
assertTrue(future.isDone());
assertEquals(busyCursor, cursorInAsync[0]);
assertEquals(busyCursor, cursorInAsync[1]);
shell.dispose();
while (!display.isDisposed() && display.readAndDispatch()) {
}
}

@Test
public void testShowWhileWithFuture() {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Shell shell = new Shell();
Display display = shell.getDisplay();
Cursor busyCursor = display.getSystemCursor(SWT.CURSOR_WAIT);
Cursor[] cursorInAsync = new Cursor[1];
CountDownLatch latch = new CountDownLatch(1);
Future<?> future = executor.submit(() -> {
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
});
// this serves two purpose:
// 1) it proves that events on the display are executed
// 2) it checks that the shell has the busy cursor during the nest showWhile.
display.asyncExec(() -> {
cursorInAsync[0] = shell.getCursor();
latch.countDown();
});
//External trigger for minimal latency as advised in the javadoc
executor.submit(()->{
try {
future.get();
} catch (Exception e) {
}
display.wake();
});
BusyIndicator.showWhile(future);
assertTrue(future.isDone());
assertEquals(busyCursor, cursorInAsync[0]);
shell.dispose();
while (!display.isDisposed() && display.readAndDispatch()) {
}
} finally {
executor.shutdownNow();
}
}

}

0 comments on commit 2d3ec43

Please sign in to comment.