Application model and CDI
A Quarkus Desktop application is a Quarkus application whose windows are CDI beans. The extension starts the user
interface once the application started, stops the application when its last window closes, and gives the ways to reach
the event dispatch thread from any thread (CompletableFuture, Mutiny, CDI events, an interceptor), without
SwingUtilities.invokeLater boilerplate nor SwingWorker.
The API is in the io.quarkiverse.desktop.awt package of quarkus-desktop-awt, for AWT and Swing applications
(quarkus-desktop-swing includes it):
| API | What for |
|---|---|
|
Observed to create the user interface, on the event dispatch thread (see Starting the user interface) |
|
Starts the user interface from the application, in the manual mode (see Manual mode) |
|
The |
|
Static helpers: |
|
Runs the methods of a bean (a presenter) on the event dispatch thread, whatever thread calls them (see @RunOnEdt) |
|
Destroys a |
|
The application menu, the Finder and the Dock of macOS as CDI events (see Application menu, Finder and Dock events (macOS)) |
A first application
import io.quarkiverse.desktop.awt.DesktopStartupEvent;
@ApplicationScoped (1)
public class Library {
public String[] titles() {
return new String[] { "Dune", "Emma", "Ulysses" };
}
}
@Singleton (2)
public class MainWindow extends JFrame {
@Inject
Library library; (3)
@ConfigProperty(name = "app.title", defaultValue = "Library")
String title;
void open(@Observes DesktopStartupEvent event) { (4)
setTitle(title);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); (5)
add(new JScrollPane(new JList<>(library.titles())));
pack();
setLocationRelativeTo(null);
setVisible(true);
}
}
| 1 | The services of the application are ordinary beans, of any scope. |
| 2 | A component (a java.awt.Component: a window, a panel…) is a @Singleton or @Dependent bean, never an
@ApplicationScoped one (see Components as CDI beans). The bean is created when the event is notified, on the event dispatch
thread: its constructor and field initializers run there. |
| 3 | Components get beans and configuration injected, as any bean. |
| 4 | Called once, on the event dispatch thread, after the application started. |
| 5 | The application stops when its last window closes (see Closing windows and exit). Never EXIT_ON_CLOSE. |
No @QuarkusMain is needed: without one, Quarkus runs until the application stops. ./mvnw quarkus:dev runs the
application in dev mode (see Dev mode), java -jar target/quarkus-app/quarkus-run.jar runs it in production, and
./mvnw package -Dnative builds a native executable (see Native executables).
Starting the user interface
An application that observes DesktopStartupEvent with @Observes has a user interface: the build finds the
observers. The other applications (a server that only renders images, for instance) have none of what this page
describes: in production, the extension does not start the AWT toolkit for them, nor install anything (dev mode and
tests start the toolkit of every application, see Dev mode).
The event is fired once, on the event dispatch thread, queued with EventQueue.invokeLater during the startup, after
the StartupEvent observers and after the look and feel of quarkus.desktop.swing.look-and-feel is set:
-
Its observers run on the event dispatch thread while the main thread goes on: the rest of the startup (the HTTP server, for instance) and the
runmethod of a@QuarkusMainmay run at the same time. Do the work that must happen before the user interface in aStartupEventobserver, or use the manual mode (see Manual mode). -
The startup of Quarkus never waits for the event dispatch thread, except to set the look and feel.
-
@Priorityorders several observers. Keep them short, they delay the first window: load the data on another thread (see Background work). -
When an observer throws (an exception or an error, such as the
LinkageErrorof a native executable missing some metadata), the error is logged and, in production, the application stops with exit code 1, instead of running without a window. -
In a headless JVM (no display, or
-Djava.awt.headless=true), the event is not fired: in production, the application logs an error and stops with exit code 1; in dev mode and tests, it logs a warning. -
The build logs a warning for an
@ObservesAsyncobserver ofDesktopStartupEvent, which is never notified. -
In tests (
@QuarkusTest), the event is not fired unless it is enabled (see Testing).
Manual mode
With the manual mode, the application fires the event itself, with DesktopLifecycle.start(), when its work before the
user interface is done: the arguments, a single instance lock, a login, or a batch mode without user interface.
quarkus.desktop.awt.startup-event.mode=manual
import io.quarkiverse.desktop.awt.DesktopLifecycle;
@QuarkusMain
public class Main implements QuarkusApplication {
@Inject
DesktopLifecycle lifecycle;
@Inject
Exporter exporter;
@Override
public int run(String... args) throws Exception {
if (args.length == 2 && args[0].equals("--export")) { (1)
exporter.export(Path.of(args[1]));
return 0;
}
lifecycle.start(); (2)
Quarkus.waitForExit(); (3)
return 0;
}
}
| 1 | A batch mode, which may run on a server without display: the user interface is never started. A headless JVM is
only reported (an error, and exit code 1 in production) when the application calls start(). |
| 2 | Queues DesktopStartupEvent on the event dispatch thread and returns at once. Only the first call fires it. |
| 3 | Waits until the application stops: when its last window closes, or when it calls Quarkus.asyncExit(). |
An application without @QuarkusMain calls DesktopLifecycle.start() from a StartupEvent observer: the event is
then fired once the application started.
@ApplicationScoped
public class Startup {
@Inject
DesktopLifecycle lifecycle;
@ConfigProperty(name = "app.user-interface", defaultValue = "true")
boolean userInterface;
void start(@Observes StartupEvent event) {
if (userInterface) { // false on a server: no window, and the AWT toolkit is not started
lifecycle.start(); // DesktopStartupEvent is fired once the application started
}
}
}
In the default auto mode, the event is queued before the run method of a @QuarkusMain is called (the work of
run is not ordered before the user interface), and DesktopLifecycle.start() does nothing.
Closing windows and exit
The application stops (Quarkus.asyncExit()) when its last visible window is closed or hidden, once a first window
opened, as JavaFX does: ShutdownEvent observers and @PreDestroy methods run, and the process exits. The windows are
counted in a later event, so an event handler may dispose a window and show another one. The close operation of a
window decides what closing it does:
| Close operation | With Quarkus Desktop |
|---|---|
|
The window is disposed. The right choice for most windows: the application stops when the last one closes, and
|
|
The window is hidden, and keeps its resources: a hidden window counts as closed. For a window shown again later, with
|
|
The application decides in a |
|
Never: it calls |
A window that asks before closing:
@Singleton
public class EditorWindow extends JFrame {
@Inject
Documents documents;
void open(@Observes DesktopStartupEvent event) {
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); (1)
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
if (documents.hasUnsavedChanges() && JOptionPane.showConfirmDialog(EditorWindow.this,
"Quit without saving?", "Quit", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
return;
}
documents.saveBounds(getBounds()); (2)
dispose(); (3)
}
});
pack();
setVisible(true);
}
}
| 1 | The window stays open unless the listener disposes it. |
| 2 | Save the window state (bounds, open documents) on WINDOW_CLOSING, on the event dispatch thread: ShutdownEvent
observers and @PreDestroy methods do not run on the event dispatch thread, and the windows may already be disposed
then. |
| 3 | The last window closed: the application stops. On macOS, Cmd-Q does not close the windows, observe QuitRequest
too (see Application menu, Finder and Dock events (macOS)). |
An application that stays alive without windows (a tray icon application, or the macOS convention of an application
without window) disables the exit policy, and calls Quarkus.asyncExit() itself:
quarkus.desktop.awt.exit-on-last-window-closed=false
@Singleton
public class TrayMenu {
void install(@Observes DesktopStartupEvent event) throws AWTException {
PopupMenu menu = new PopupMenu();
MenuItem quit = new MenuItem("Quit");
quit.addActionListener(e -> Quarkus.asyncExit()); (1)
menu.add(quit);
TrayIcon icon = new TrayIcon(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/tray.png")),
"Library", menu);
icon.setImageAutoSize(true);
SystemTray.getSystemTray().add(icon); (2)
}
}
| 1 | Stops the application: ShutdownEvent observers and @PreDestroy methods run. |
| 2 | Check SystemTray.isSupported() first on Linux, where some desktops have no system tray. In dev mode and tests, the
tray icons are removed when the application stops. |
The exit policy is also for a splash screen: show the main window before closing the splash screen, or the application stops in between (or disable the policy). It is never installed in tests, nor with Quarkus FX (see Quarkus FX).
The event dispatch thread
AWT and Swing components are created and used on the event dispatch thread. The DesktopStartupEvent observers, the
listeners of the components, the events of java.awt.desktop and @RunOnEdt methods run there; everything else
(StartupEvent observers, REST resources, @Scheduled methods, messaging, ShutdownEvent observers) runs on other
threads. The extension gives three ways to get to the event dispatch thread:
-
EdtExecutor, the executor of the event dispatch thread: continue there after work done on another thread; -
Edt, static helpers, including the only call that waits for the event dispatch thread; -
@RunOnEdt, an interceptor binding: the methods of a bean always run there, whatever thread calls them.
Never block the event dispatch thread (a database query, an HTTP call, a file read): the user interface freezes.
Background work
Run the work on a Quarkus executor and continue on the event dispatch thread with EdtExecutor, an Executor bean
that always queues the task (EventQueue.invokeLater), in order, even when called on the event dispatch thread:
import io.quarkiverse.desktop.awt.EdtExecutor;
@Singleton
public class BooksWindow extends JFrame {
@Inject
BookRepository repository;
@Inject
ManagedExecutor workers; (1)
@Inject
EdtExecutor edt; (2)
private final DefaultListModel<String> books = new DefaultListModel<>();
private final JLabel status = new JLabel();
void open(@Observes DesktopStartupEvent event) {
add(new JScrollPane(new JList<>(books)));
add(status, BorderLayout.SOUTH);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setVisible(true);
refresh();
}
void refresh() { // on the event dispatch thread
status.setText("Loading...");
CompletableFuture.supplyAsync(repository::titles, workers) (3)
.whenCompleteAsync((titles, error) -> { (4)
if (error != null) {
status.setText("Loading failed: " + error.getMessage());
} else {
show(titles);
}
}, edt);
}
void search(String text) { // on the event dispatch thread
repository.search(text) // a Uni<List<String>>
.emitOn(edt) (5)
.subscribe().with(this::show, error -> status.setText(error.getMessage()));
}
void reload(@ObservesAsync BooksChanged event) { (6)
refresh();
}
private void show(List<String> titles) {
books.clear();
books.addAll(titles);
status.setText(titles.size() + " books");
}
}
| 1 | The ManagedExecutor of quarkus-smallrye-context-propagation propagates the contexts (CDI request context,
transactions) to the worker threads. Without it, @Inject ExecutorService gets the worker pool of Quarkus. |
| 2 | The only bean type of EdtExecutor is EdtExecutor: @Inject Executor still gets the worker pool of Quarkus. In
static code, EventQueue::invokeLater is the same executor. |
| 3 | Loads the data on a worker thread. |
| 4 | Continues on the event dispatch thread, with the result or the error. |
| 5 | Mutiny: the items and the failure are emitted on the event dispatch thread. Use runSubscriptionOn(workers) for a
Uni that blocks when subscribed. |
| 6 | Asynchronous events fired with the EdtExecutor are notified on the event dispatch thread, see below. |
An asynchronous CDI event fired with NotificationOptions.ofExecutor(edt) notifies its @ObservesAsync observers on
the event dispatch thread, from any thread:
@ApplicationScoped
public class BookService {
@Inject
BookRepository repository;
@Inject
Event<BooksChanged> booksChanged;
@Inject
EdtExecutor edt;
public void save(String title) { // on any thread
repository.save(title);
booksChanged.fireAsync(new BooksChanged(), NotificationOptions.ofExecutor(edt)); (1)
}
}
| 1 | Every @ObservesAsync BooksChanged observer runs on the event dispatch thread. A synchronous event (fire) runs
its observers on the thread that fires it. |
Edt
Edt has static helpers:
-
Edt.isEdt(): whether the current thread is the event dispatch thread. -
Edt.run(task): runs the task now when called on the event dispatch thread, later (EventQueue.invokeLater) otherwise. -
Edt.call(callable, timeout): the only call that waits for the event dispatch thread, for background threads and tests. Called on the event dispatch thread, it calls the task now. It throws aTimeoutExceptioninstead of waiting for ever, and the exception of the task.
Dimension size = Edt.call(panel::getPreferredSize, Duration.ofSeconds(5));
Edt.run(() -> status.setText("Saved"));
When Edt.call times out, or the waiting thread is interrupted, a task that did not start yet is skipped; a task
already running goes on, and its result is dropped. Never call it while holding a lock that the event dispatch thread
may need: the AWT tree lock, a lock of the application, or ArC creating a bean (a constructor, a @PostConstruct
method, a producer, see Components as CDI beans). Nor from a shutdown hook or a ShutdownEvent observer, where the event
dispatch thread may be blocked. When the name Edt clashes with a class of the application, use the qualified name
io.quarkiverse.desktop.awt.Edt.
@RunOnEdt
@RunOnEdt runs the methods of a bean on the event dispatch thread, whatever thread calls them (a @Scheduled method,
a REST resource, a Kafka consumer, an asynchronous observer). Put it on presenters (beans that are not components), or
on methods:
import io.quarkiverse.desktop.awt.RunOnEdt;
public interface StatusView {
void showStatus(String text);
boolean confirm(String question);
}
@Singleton
public class StatusBar extends JLabel implements StatusView { (1)
@Override
public void showStatus(String text) {
setText(text);
}
@Override
public boolean confirm(String question) {
return JOptionPane.showConfirmDialog(this, question, "Confirm",
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION;
}
}
@ApplicationScoped
public class StatusPresenter {
@Inject
Instance<StatusView> view; (2)
@RunOnEdt
public void show(String text) { (3)
view.get().showStatus(text);
}
@RunOnEdt
public CompletionStage<Boolean> confirm(String question) { (4)
return CompletableFuture.completedFuture(view.get().confirm(question));
}
}
@ApplicationScoped
public class ServerMonitor {
@Inject
StatusPresenter presenter;
@Inject
ServerClient server;
@Scheduled(every = "10s")
void poll() { // on a worker thread
presenter.show(server.status());
}
}
| 1 | The view: a component bean, added to the main window, that implements an interface the presenter uses (which makes the presenter testable, see Testing). |
| 2 | Get the component beans on the event dispatch thread, in the method (Instance<T> or Provider<T>): a component
bean injected directly is created with the presenter, on the thread that first uses it. The build logs a warning for it. |
| 3 | A void method runs now when called on the event dispatch thread, later (EventQueue.invokeLater) otherwise: the
call returns at once, and an exception goes to the uncaught exception handler (see Uncaught exceptions). |
| 4 | A method returning a CompletionStage (or a CompletableFuture) returns a stage completed with its result, or its
exception, once it ran on the event dispatch thread. |
The build fails for a @RunOnEdt method returning another type (there is no blocking mode: use Edt.call), and for a
bean that is not @ApplicationScoped, @Singleton or @Dependent (a @RequestScoped instance belongs to the request
of the caller, which may be over when the method runs). @RunOnEdt on a class applies to its business methods; on a
component class it is a build error, annotate the methods (see Components as CDI beans).
The interceptor of @RunOnEdt is the outermost one (priority PLATFORM_BEFORE - 100), so the other interceptors of the
method run on the event dispatch thread, around the method:
| Interceptor | Where it runs with @RunOnEdt |
|---|---|
|
On the calling thread: it queues the call |
|
On the event dispatch thread, around the method: a transaction or a retry then blocks the event dispatch thread. Call
such methods from a worker thread, and |
Fault tolerance |
On the event dispatch thread: when the timeout expires, it interrupts the event dispatch thread (AWT then replaces it). The build logs a warning |
Fault tolerance |
The method runs on another thread, not on the event dispatch thread. The build logs a warning |
Tracing ( |
On the event dispatch thread: the span of a queued call has no parent (the context of the calling thread is not propagated) |
The event dispatch thread has no active request context: use @ActivateRequestContext on a method that needs one.
@RunOnEdt works on static methods too, with the same return types (the build fails for a private static method,
which ArC does not intercept). A queued call is skipped when the application stopped before it ran (dev mode restarts,
tests); a call on a @Dependent bean already destroyed still runs.
Uncaught exceptions
An exception that escapes a listener, or a void @RunOnEdt method, is logged at the ERROR level in the
io.quarkiverse.desktop.awt.edt category, with the name of the thread, instead of being printed on the standard
error: it goes to the Quarkus log handlers (console, file, JSON). The event dispatch thread keeps running after it.
The extension installs this default uncaught exception handler (Thread.setDefaultUncaughtExceptionHandler) when an
application with a user interface starts, only when the JVM has none, so it covers the other threads too; an
application that installs its own handler replaces it. In dev mode and in tests, the previous handler is restored when
the application stops.
# keep the log (and the uncaught exceptions) of a desktop application in a file
quarkus.log.file.enabled=true
# or do not log them
quarkus.log.category."io.quarkiverse.desktop.awt.edt".level=OFF
Components as CDI beans
AWT and Swing components can be CDI beans, with injection, configuration, @PostConstruct and @PreDestroy, when:
-
They are
@Singleton(a main window, a status bar) or@Dependent(a dialog or a document window opened several times, a panel of a window) beans, never normal scoped ones (@ApplicationScoped,@RequestScoped…): the client proxy of a normal scoped bean is a subclass of the component, so creating the proxy creates another component, outside the event dispatch thread, and the proxy of a Swing component cannot even be loaded (it overrides the final methods ofJComponent). -
They are created on the event dispatch thread: in a
DesktopStartupEventobserver (a component bean that observes it is created on the event dispatch thread), a@RunOnEdtmethod, a listener, or throughInstance<T>orProvider<T>resolved on the event dispatch thread. A component bean injected directly into another bean is created with that bean, on the thread that first uses it: inject components directly only into other components created on the event dispatch thread. -
Their constructor,
@PostConstructmethod or producer never waits for the event dispatch thread (SwingUtilities.invokeAndWait,Edt.call): ArC holds a lock while it creates a bean, and an observer of that bean notified on the event dispatch thread at the same time waits for that lock, a deadlock.
The logic goes to presenters and services: beans that are not components, of any scope (@ApplicationScoped is fine
there), which use the components on the event dispatch thread (@RunOnEdt, EdtExecutor) and are easy to test and
mock (see Testing).
The build checks these rules. Errors for what cannot work, warnings for what may not:
| Found | Result | Instead |
|---|---|---|
A normal scoped component bean (a class or a producer) |
Build error |
|
An interceptor binding on a component class ( |
Build error |
Annotate methods, or move the logic to a presenter |
A |
Build error |
Return |
|
Build error |
One of these scopes |
A component bean injected directly into a |
Build warning |
|
A |
Build warning |
|
|
Build warning |
See the message |
A component bean that exists already when |
Warning at run time |
Create it on the event dispatch thread |
Window beans
Windows that the application opens several times (dialogs, document windows) are @Dependent beans got from an
Instance<T>. WindowBeans.get(instance) gets one on the event dispatch thread, and destroys the bean once its window
is closed: its @PreDestroy methods run (stop its timers, remove its global listeners) and its own dependent beans are
destroyed. With Instance.get() alone, the Instance keeps every dialog it created until it is destroyed itself.
import io.quarkiverse.desktop.awt.WindowBeans;
@Dependent
public class InvoiceDialog extends JDialog {
@Inject
InvoiceService service;
private final Timer refresh = new Timer(5_000, e -> reload());
private Invoice invoice;
InvoiceDialog() {
setDefaultCloseOperation(DISPOSE_ON_CLOSE); (1)
}
void edit(Invoice invoice) {
this.invoice = invoice;
setTitle("Invoice " + invoice.number());
refresh.start();
}
private void reload() {
invoice = service.reload(invoice);
}
@PreDestroy
void close() { (2)
refresh.stop();
}
}
@Singleton // not @ApplicationScoped
public class InvoicesWindow extends JFrame {
@Inject
InvoiceService service;
@Inject
Instance<InvoiceDialog> dialogs;
void open(@Observes DesktopStartupEvent event) {
JList<Invoice> invoices = new JList<>(service.findAll());
JButton edit = new JButton("Edit");
edit.addActionListener(e -> {
InvoiceDialog dialog = WindowBeans.get(dialogs); (3)
dialog.edit(invoices.getSelectedValue());
dialog.pack();
dialog.setVisible(true);
});
add(new JScrollPane(invoices));
add(edit, BorderLayout.SOUTH);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setVisible(true);
}
}
| 1 | The bean is destroyed when its window is disposed (WINDOW_CLOSED), not when it is hidden: a JDialog hides on
close by default (HIDE_ON_CLOSE). A window that is never shown nor packed is never closed: when the code that shows
it may fail first, destroy the bean yourself (dialogs.destroy(dialog)). |
| 2 | Called on the event dispatch thread when the window closes. When the application stops in dev mode and tests, the windows are disposed before the container stops, and their beans are destroyed on the event dispatch thread too (unless the event dispatch thread is blocked for more than 5 seconds). The container destroys the remaining beans on the shutdown thread, and the windows closed afterwards do not destroy them again. |
| 3 | WindowBeans.get throws an IllegalStateException outside the event dispatch thread. A @Singleton window is
returned as is, and never destroyed. |
Application menu, Finder and Dock events (macOS)
The application handlers of java.awt.Desktop are CDI events: observe the event types of the JDK (package
java.awt.desktop) in any bean, and QuitRequest for the quit requests. The observers run on the event dispatch
thread.
import java.awt.desktop.AboutEvent;
import java.awt.desktop.AppReopenedEvent;
import java.awt.desktop.OpenFilesEvent;
import java.awt.desktop.OpenURIEvent;
import java.awt.desktop.PreferencesEvent;
import io.quarkiverse.desktop.awt.QuitRequest;
import io.quarkiverse.desktop.awt.WindowBeans;
@Singleton
public class ApplicationMenu {
@Inject
Instance<AboutDialog> about;
@Inject
Instance<PreferencesDialog> preferences;
@Inject
Instance<MainWindow> main;
@Inject
Documents documents;
void about(@Observes AboutEvent event) { (1)
WindowBeans.get(about).setVisible(true);
}
void preferences(@Observes PreferencesEvent event) {
WindowBeans.get(preferences).setVisible(true);
}
void open(@Observes OpenFilesEvent event) { (2)
event.getFiles().forEach(documents::open);
}
void open(@Observes OpenURIEvent event) { (3)
documents.open(event.getURI());
}
void reopen(@Observes AppReopenedEvent event) { (4)
main.get().setVisible(true);
main.get().toFront();
}
void quit(@Observes QuitRequest request) { (5)
if (documents.hasUnsavedChanges() && JOptionPane.showConfirmDialog(main.get(), "Quit without saving?", "Quit",
JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
request.cancel();
}
}
}
| 1 | The About and Preferences items of the application menu. Observing AboutEvent replaces the default About window
of the JDK; without an observer of PreferencesEvent, the Preferences item is disabled. |
| 2 | The files opened with the application (from the Finder, or dropped on its Dock icon), including the file that
started it: the JDK keeps them until the handler is installed, and the extension installs it just before firing
DesktopStartupEvent, so they are fired once the main window is created. PrintFilesEvent is the same for printing. |
| 3 | A URL of a scheme of the application (myapp://…). |
| 4 | The Dock icon clicked while the application runs (for instance with no window visible, see
quarkus.desktop.awt.exit-on-last-window-closed). |
| 5 | Quit in the application menu, Cmd-Q, Quit in the Dock, a logout or a shutdown. |
How it works:
-
Quit: on macOS, the extension always installs a quit handler (even without an observer of
QuitRequest). It firesQuitRequest, then the application stops withQuarkus.asyncExit(0)unless an observer calledrequest.cancel():ShutdownEventobservers and@PreDestroymethods run, as when the last window closes. Without it, the JDK would callSystem.exiton the AppKit thread. An observer that throws cancels the quit (the error is logged): an observer guarding unsaved changes never loses them. -
Logout and shutdown: a cancelled quit is replied to macOS at once ("do not terminate"), which also cancels a logout or a shutdown of the Mac: cancel only when the user asked to. Otherwise, in production, the reply stays pending while Quarkus stops the application, as with the default handler of the JDK: macOS waits for the process to exit, and the logout or the shutdown goes on. In dev mode and tests, where the JVM outlives the application, the extension replies "do not terminate" and the application stops.
-
Only what is observed: the handlers are installed for the events that the application observes (found at build time), when the application has a user interface (see Starting the user interface). So they are not installed in tests unless the startup event is enabled. The build logs a warning for an observer of a
java.awt.desktopevent without an observer ofDesktopStartupEvent, and for the events that are not fired (@ObservesAsyncobservers,QuitEvent: observeQuitRequest, and the events of the other listeners, see below). -
Platforms: macOS supports all these events. Windows and Linux support none of them: the observers are never called there, and the extension does not even call
java.awt.Desktopon Linux (it loads GTK). On Windows and Linux there are no quit requests: closing the last window stops the application, ask for confirmation onWINDOW_CLOSING(see Closing windows and exit). -
Other events:
AppForegroundEvent,AppHiddenEvent,ScreenSleepEvent,SystemSleepEventandUserSessionEventare not fired (one event type is used for two notifications, for instance moved to the foreground and to the background): register a listener withDesktop.getDesktop().addAppEventListener(…)in aDesktopStartupEventobserver, and remove it in a@PreDestroymethod for dev mode. An application may also set its own handlers there (Desktop.getDesktop().setQuitHandler(…)): they replace those of the extension for the rest of the run (java.awt.Desktophas no getter to restore them). Setting a handler tonullrestores the one of the JDK, not the one of the extension: prefer observing the CDI events, and check a flag in the observer to react only while a view is shown. -
Dev mode and tests: the handlers are removed when the application stops, which restores those of the JDK.
-
Native executables: the events work the same. The files and URLs opened with the application need the information property list of an application bundle declaring the document types (
CFBundleDocumentTypes) and the URL schemes (CFBundleURLTypes):quarkus.desktop.awt.macos.info-plistdoes not declare them, write theInfo.plistof the bundle.
Dev mode
./mvnw quarkus:dev runs the application in dev mode: a restart runs it again in the same JVM, and
DesktopStartupEvent is fired again, to the new beans, which open the windows again.
What a restart disposes: when the application stops (in dev mode, and in tests, where several applications run in
the same JVM), the extension disposes the windows and removes the tray icons of the application, on the event dispatch
thread, after the ShutdownEvent observers and before the CDI container stops (the shutdown waits for the event
dispatch thread 5 seconds at most). The @Dependent window beans of WindowBeans are destroyed with their windows.
The exit policy, the handlers of java.awt.Desktop and the uncaught exception handler of the extension are removed,
and @RunOnEdt calls still queued are skipped.
What stays: the AWT toolkit and its threads (the event dispatch thread gets the class loader of the running
application, see Dev mode), the Dock icon on macOS, and the global AWT and Swing state: the
look and feel set with UIManager.setLookAndFeel (quarkus.desktop.swing.look-and-feel is set again at each start),
UIManager.put values, the listeners added to Toolkit or KeyboardFocusManager, running javax.swing.Timer and
threads. Stop and remove what the application started outside its windows in @PreDestroy methods, or they keep the
code of the previous run alive.
Restarts: dev mode looks for changes of the sources when the application gets an HTTP request, and [s] in the
console forces a restart (the windows are closed and opened again). A desktop application has no HTTP requests unless it
uses an HTTP extension. With one (for instance quarkus-vertx-http, which also brings the Dev UI), the instrumentation
of dev mode swaps the changes of method bodies without a restart, the windows staying open with their state: change the
code, then open any page of the application (for instance http://localhost:8080/q/dev-ui).
# method body changes are swapped in the running application, without a restart ([i] toggles it in the console)
quarkus.live-reload.instrumentation=true
Exit: when the last window closes, or on Cmd-Q on macOS, the application stops (Quarkus.asyncExit()) and dev mode
keeps running: press [space] in the console to start the application again. Never use JFrame.EXIT_ON_CLOSE, which
exits the JVM and ends dev mode.
Continuous testing: the tests of continuous testing run in another application of the same JVM. When they enable the
user interface (see Testing), the windows opened while they run are disposed once they ran, and the handlers of
java.awt.Desktop stay those of the dev mode application. The exit policy of the dev mode application counts the
windows of the tests while they are shown.
Testing
In tests (@QuarkusTest), DesktopStartupEvent is not fired: the application starts without its windows, the exit
policy and the handlers of java.awt.Desktop, and its beans (services, presenters) are tested as in any Quarkus
application. quarkus.arc.test.disable-application-lifecycle-observers=true disables it even when it is enabled.
User interface tests enable the event (for all the tests, or in a test profile), and need a display:
%test.quarkus.desktop.awt.startup-event.enabled=true
import io.quarkiverse.desktop.awt.Edt;
@QuarkusTest
@DisabledIf("java.awt.GraphicsEnvironment#isHeadless") (1)
class MainWindowTest {
@Inject
Instance<MainWindow> window; (2)
@Test
void opens() throws Exception {
assertTrue(Edt.call(() -> window.get().isShowing(), Duration.ofSeconds(5))); (3)
assertEquals("Library", Edt.call(() -> window.get().getTitle(), Duration.ofSeconds(5)));
}
}
| 1 | Skipped without display: in a headless JVM, the event is not fired (the application logs a warning). On Linux, run
the tests on a virtual display (xvfb-run ./mvnw verify). |
| 2 | The window is created by its DesktopStartupEvent observer, on the event dispatch thread: get it there, never
inject the component bean into the test (it would be created on the test thread if the test ran first). |
| 3 | Edt.call runs after the startup event, queued before it, and reads the components on the event dispatch thread. |
In tests, the application does not stop when its windows close, and its windows are disposed when it stops (between
test profiles, for instance). In the manual mode, @QuarkusTest does not run the @QuarkusMain: call
DesktopLifecycle.start() in the test (only the first call fires the event), or launch the main with
@QuarkusMainTest.
Presenters: QuarkusMock and @InjectMock only replace normal scoped beans, not the @Singleton components. Make
the presenters depend on view interfaces rather than on the component classes, and test them with
@QuarkusComponentTest (quarkus-junit-component), which mocks the views, without display:
@QuarkusComponentTest
class StatusPresenterTest {
@Inject
StatusPresenter presenter;
@InjectMock
StatusView view; (1)
@Test
void show() {
presenter.show("Ready"); (2)
Mockito.verify(view).showStatus("Ready");
}
@Test
void confirm() throws Exception {
Mockito.when(view.confirm("Delete?")).thenReturn(false);
assertFalse(presenter.confirm("Delete?").toCompletableFuture().get());
}
}
| 1 | The mock of the view that the presenter gets from its Instance<StatusView>. |
| 2 | A component test runs the beans without the Quarkus extensions: the interceptor of @RunOnEdt is not there, and
the method runs on the test thread. |
Native executables
The CDI integration needs nothing in native executables: there is nothing to register. The beans, observers,
interceptors (@RunOnEdt) and events are generated at build time by ArC, and the extension takes care of its own
classes. The window classes of the
application may need metadata for other reasons, as in any AWT or Swing application (the JavaBeans API, classes loaded
by name, see Limitations).
When a DesktopStartupEvent observer fails in a native executable (a LinkageError or a
MissingReflectionRegistrationError of missing metadata), the error is logged and the application stops with exit code
1, instead of running without a window.
Quarkus FX
With Quarkus FX in the application (AWT or Swing in a JavaFX application,
SwingNode, JFXPanel), JavaFX owns the application:
-
The exit policy is disabled: JavaFX decides when the application stops. This is also the case for a Swing application that embeds JavaFX with
JFXPanel(with its own@QuarkusMain, see the Quarkus FX documentation): callQuarkus.asyncExit()when its main window closes. -
No handler of
java.awt.Desktopis installed: on macOS, AWT runs embedded in the JavaFX application and they are not called, use the JavaFX APIs. -
The other features do not depend on Quarkus FX:
EdtExecutor,Edtand@RunOnEdtfor the event dispatch thread, next to@RunOnFxThreadfor the JavaFX application thread. -
In the
automode,DesktopStartupEventstarts the AWT toolkit during the startup of Quarkus, before JavaFX starts. On macOS, prefer themanualmode, and start the user interface once JavaFX started:
import io.quarkiverse.desktop.awt.DesktopLifecycle;
import io.quarkiverse.fx.FxPostStartupEvent;
@ApplicationScoped
public class DesktopStart {
@Inject
DesktopLifecycle lifecycle;
void start(@Observes FxPostStartupEvent event) { // on the JavaFX application thread, once JavaFX started
lifecycle.start(); // DesktopStartupEvent is fired on the event dispatch thread
}
}
This combination is not verified yet on macOS.
Configuration
These run time properties also apply in JVM mode:
| Property | Default | Description |
|---|---|---|
|
|
Whether |
|
|
|
|
|
Whether the application stops when its last visible window is closed or hidden (see Closing windows and exit). Never in tests, nor with Quarkus FX. |
See the configuration reference for the details.