Class Dialog

java.lang.Object
limn.components.Dialog

public final class Dialog extends Object
A modal (or non-modal) dialog that resolves a CompletionStage with the chosen button's result: never a nested event loop, so show* returns immediately and the stage completes (on the UI thread) later.

Two display modes (setDisplayMode(limn.components.DisplayMode)):

  • DisplayMode.NATIVE_WINDOW (default): a separate floating native window centered over the owner, with a WindowStyle and optional always-on-top. Modality is enforced by the backend (locked windows dim and ignore input). Not available to a modal dialog over a fullscreen owner; see below.
  • DisplayMode.IN_SCENE: an internal overlay drawn inside the owner window: a scrim that dims/blocks the content behind plus a centered card that fades in. No extra OS window; modality is scoped to that scene.

Three modality scopes (native window mode):

  • show(Scene) is window-modal: locks only the owner window (and its owned popups); other windows stay interactive.
  • showToolkitModal(Scene) is toolkit-modal: locks every window in the application.
  • showNonModal(Scene) is non-modal: locks nothing; a floating panel alongside a fully interactive owner.
In IN_SCENE mode the overlay always captures its own scene's input; the owner stays interactive (it hosts the overlay) but its sibling windows are locked like a native modal: show locks the owner's owned popups, showToolkitModal locks every window. showNonModal is unsupported in-scene (use a native window for a non-modal dialog).

Over a fullscreen owner a modal dialog is drawn in scene, whatever was asked, for the reasons DisplayMode gives: an owner fullscreen at a named mode is minimized as soon as the dialog's window takes the focus, which it does on opening; one at the mode the display is in already covers the screen, so the card loses nothing drawn inside it; and on a Mac without a GPU the colour picker's dialog held the UI for 1.4 to 3.7 seconds as a window, against 0.6 to 0.7 in scene. Its scope is kept: showToolkitModal still locks every other window. A non-modal dialog is the exception, and stays a window even there: an overlay captures its window's input, and a non-modal dialog exists so that the owner stays usable, which leaves it no in-scene form to fall back to. A window that cannot be positioned (Wayland) changes nothing: a dialog has no anchor to line up with, so the compositor placing it is where it belongs. displayMode() answers what was chosen.

The same holds for a modal dialog already open as a window when its owner goes fullscreen: it moves into the scene then, card, contents, focus and scope, and is still waiting for its answer; the owner going fullscreen answers nothing. It is left a window only while another dialog or a modal menu is open over it, since moving it would close the window that one belongs to, and nothing moves back into a window when the owner leaves fullscreen.

Stacking

Dialogs stack in either mode and in any combination: the newest is the one the user answers, everything under it is frozen until it closes, and answering the top one hands control back to the one below.

One exception, and it changes the display mode: an IN_SCENE dialog raised over a window that a modal has already locked is presented as a native window instead, with a warning naming the dialog. An overlay is drawn inside its host window, so it can only come forward by bringing that window forward, which would hide the dialog already floating over it. keepInScene() declines the promotion, and a fullscreen owner never makes it, since a window there does not stack over anything.

Asking before it is cancelled

A dialog the user closes with nothing chosen, by Escape, a click on the scrim, a button whose result is the cancel result, the system closing its own window or a screen reader's dismiss, asks its onCancel handler first when it has one. The handler answers now or later, so a form with unsaved changes can ask "Save changes?" over itself and close only once that is answered. Every other answer closes it at once, and so does dismiss(java.lang.String).

Size

A dialog is a container of sized components rather than a sized component: it has no size knob of its own, it is a ControlSize scope that its content inherits. Its three gutters and its dialogMaxWidth measure cap all follow the resolved step.

Because every show* takes a Scene, the step is inherited from the owner scene's root, not from the control that opened the dialog, so a dialog raised from a SMALL toolbar inside a MEDIUM scene renders MEDIUM. Declare it with setControlSize(limn.scene.ControlSize), or pass the opening widget to show(Widget), to get anything else.

A dialog is also bounded, and never grows past what it can be seen in: a native window is capped at the work area of the display it opens on (the monitor less the OS chrome), and an in-scene overlay at the window hosting it, both inset by one gutter so a capped card reads as capped rather than as clipped. Content that does not fit scrolls, and the button row is deliberately outside the scrolling part, so the way out of a dialog can never be scrolled away. A dialog that fits is unaffected: it is exactly as tall as it asks to be, and has nothing to scroll.

A Dialog is single-use: it presents once and resolves once. Calling any show* method a second time (including after the dialog closed) throws IllegalStateException; build a new instance per presentation.


 new Dialog("Delete?", "This action cannot be undone.")
     .addPrimaryButton("Keep", "cancel")
     .addDestructiveButton("Delete", "delete")
     .show(scene)
     .thenAccept(result -> { if ("delete".equals(result)) ...; });
 
  • Constructor Details

    • Dialog

      public Dialog(String title, String message)
      A dialog with fixed text; see the I18nString constructor for localized text.
      Parameters:
      title - the title, drawn on the card and read as the dialog's name; never null
      message - the sentence under the title; empty for none, never null
    • Dialog

      public Dialog(I18nString title, I18nString message)
      A dialog whose title and message follow the UI language; see I18nString.
      Parameters:
      title - the title, drawn on the card and read as the dialog's name; never null
      message - the sentence under the title; empty for none, never null
  • Method Details

    • setContent

      public Dialog setContent(@Nullable Widget<?> widget)
      Puts a widget between the message and the buttons: a form, a preview, a picker. A dialog is the toolkit's one modal surface, and until now the only thing it could say was a sentence; anything that needs an answer richer than a button had to build its own overlay and re-solve focus, dismissal and the scrim.

      The widget inherits the card's ControlSize like everything else in it. Passing null removes the current one. UI thread only.

      Parameters:
      widget - the content, or null to remove it
      Returns:
      this dialog
    • addButton

      public Dialog addButton(String text, @Nullable String resultValue)
      Adds a secondary (outlined) button that resolves the dialog with resultValue.
      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • addButton

      public Dialog addButton(I18nString text, @Nullable String resultValue)
      A secondary button whose caption follows the UI language.
      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • addPrimaryButton

      public Dialog addPrimaryButton(String text, @Nullable String resultValue)
      Adds a primary (filled) button that resolves the dialog with resultValue.

      The first one added is also the card's default button: Return resolves the dialog with its result whenever the focused widget did not want the key itself, so Return in a text field answers the dialog, while Return on a focused button presses that button and Return in a TextArea still inserts a newline.

      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • addPrimaryButton

      public Dialog addPrimaryButton(I18nString text, @Nullable String resultValue)
      A primary button whose caption follows the UI language.
      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • addDestructiveButton

      public Dialog addDestructiveButton(String text, @Nullable String resultValue)
      Adds a button that destroys something ("Delete", "Discard") and resolves the dialog with resultValue: a secondary button with its caption in the palette's danger colour.

      It is never the default button, whatever was added before it, and Return does not press it, even while it holds the focus: so a person typing into the dialog and pressing Return out of habit does not delete. Return then gives the default button's answer, or none in a dialog without one. Nor does it take the dialog's first focus, which goes to the widget the content asked for as it joined (Scene.requestFocus(limn.scene.Widget<?>) from its onAttached), else to the first control of the content that takes the keyboard (a field, say), or, when the content has none, to the default button, or else to the first button that destroys nothing; only in a dialog whose every button is destructive does the first of them take it, Return still not pressing it and Escape still cancelling. Space, a click and an assistive technology's press do press it. Give the dialog a primary button for the safe answer, which Return then gives.

      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • addDestructiveButton

      public Dialog addDestructiveButton(I18nString text, @Nullable String resultValue)
      A destructive button whose caption follows the UI language; see addDestructiveButton(String, String).
      Parameters:
      text - the caption; never null
      resultValue - what the dialog resolves with when this button is pressed; may be null
      Returns:
      this dialog
    • setCancelResult

      public Dialog setCancelResult(@Nullable String value)
      Sets the result delivered on ESC / scrim dismiss (default null): the answer that means nothing was chosen. A button whose result is this one cancels too, and every way of cancelling asks onCancel first when it is set.
      Parameters:
      value - what Escape or a dismissing scrim click resolves with; may be null
      Returns:
      this dialog
    • onCancel

      public Dialog onCancel(@Nullable Consumer<Dialog.CancelRequest> handler)
      Asks the application whether the dialog may close when the user closes it with nothing chosen, which is every way of giving the cancel result: Escape, a click on the scrim of a dialog that dismisses on it, a button whose result is the cancel result (judged when it is pressed, Return on the default button included), the system closing the dialog's own window, and an assistive technology's dismiss. The handler is given a Dialog.CancelRequest it answers now or later; without a handler the dialog closes. An answer other than the cancel result, the Save of a question asked over a form, is given with the request's dialog's dismiss(java.lang.String), which settles the request. Its other buttons, a destructive one included, are answers and never asked about, and neither is dismiss(java.lang.String), the application's own close, nor the dialog's window or its owner going away, nor the application ending.

      The handler is called on the UI thread, during the user's gesture, before anything fades. While its request is unanswered the dialog asks nothing new: a second Escape does nothing. A handler that throws has refused: the dialog stays open, and the exception reaches the crash handler as any exception from application code on the UI thread does.

      Parameters:
      handler - the handler, or null to clear the slot
      Returns:
      this dialog
      Throws:
      IllegalStateException - if a handler is already registered
    • setControlSize

      public Dialog setControlSize(@Nullable ControlSize size)
      Declares the size step for the whole card, overriding the inherited one: the only size knob a dialog has, and a forwarder rather than an override because a Dialog is not a Widget: it lands on the card's root, which is what every control inside the dialog then inherits, in both display modes.

      null restores inheritance from the owner scene. UI thread only.

      Parameters:
      size - the step, or null to inherit it from the owner
      Returns:
      this dialog
    • controlSize

      public ControlSize controlSize()
      Returns:
      the step the card currently resolves to (declared, inherited or default)
    • setDisplayMode

      public Dialog setDisplayMode(DisplayMode mode)
      Selects native-window vs. internal (in-scene) presentation (default DisplayMode.NATIVE_WINDOW).

      A preference, not a guarantee, in both directions. An IN_SCENE dialog raised while a modal is already open over its window is presented as a native one instead; an overlay can only be brought to the front by raising its host, which would hide the dialog already there. And a modal NATIVE_WINDOW dialog over a fullscreen window is drawn in scene, for the reasons DisplayMode gives, and one already a window moves into the scene when its owner goes fullscreen; a non-modal one stays a window, having no in-scene form. displayMode() answers what actually happened; keepInScene() insists on the overlay.

      Parameters:
      mode - the presentation asked for
      Returns:
      this dialog
    • displayMode

      public DisplayMode displayMode()
      How this dialog is actually presented: the requested mode until show(limn.scene.Scene) decides otherwise, and the effective one afterwards, which changes once more, to DisplayMode.IN_SCENE, if a modal dialog's owner goes fullscreen while it is a window.

      Worth asking after show by anything that styles or positions the card on the assumption that it is one or the other: a promoted dialog is a real window, so setStyle starts meaning something and an in-scene drag affordance stops, and a modal dialog raised over a fullscreen window is an overlay whatever was asked, so the reverse. Until this existed the promotion was announced only in a log line, which no application reads.

    • keepInScene

      public Dialog keepInScene()
      Insists on DisplayMode.IN_SCENE even when a modal window is already open over the host, the case this dialog would otherwise be promoted out of, silently and with a warning.

      Say this when you have looked at the stacking and are content with it: the overlay will be answerable, but bringing its host window forward hides any dialog floating over it, so the earlier one appears to vanish until this one closes. The honest reason to accept that is not needing an extra OS window at all: an embedded or kiosk surface, or a host that cannot create one.

      No boolean: promotion is the default and this is an assertion, not a property. keepInScene(false) would be a way to write "promote me, probably", which is just the default spelled less clearly.

    • setAlwaysOnTop

      public Dialog setAlwaysOnTop(boolean value)
      Whether the native dialog window floats above other windows (default true). Only applies to DisplayMode.NATIVE_WINDOW.
      Parameters:
      value - whether the window floats above others
      Returns:
      this dialog
    • setDismissOnScrim

      public Dialog setDismissOnScrim(boolean value)
      Whether clicking the scrim dismisses the dialog (default false). Only applies to DisplayMode.IN_SCENE. When false the dialog is truly modal: a scrim click is ignored and answered with the same alert feedback (beep) a native modal gives; when true it closes with the cancel result (a dismissable overlay), asking onCancel first when it is set.
      Parameters:
      value - whether a scrim click dismisses
      Returns:
      this dialog
    • setStyle

      public Dialog setStyle(WindowStyle newStyle)
      Sets the native window framing (default WindowStyle.UNDECORATED_TRANSLUCENT): DECORATED uses the OS title bar/border (opaque); UNDECORATED_OPAQUE is a borderless solid card; the default is a borderless glassy panel with rounded, see-through corners. Ignored in DisplayMode.IN_SCENE (the internal card is always the glassy panel).
      Parameters:
      newStyle - the framing
      Returns:
      this dialog
    • dismiss

      public void dismiss(@Nullable String result)
      Programmatically closes the dialog, resolving with result. UI thread only.

      Never asks the onCancel handler, whatever the result, and settles a request that handler holds: it is how a handler gives an answer other than the cancel result.

      Parameters:
      result - what the dialog resolves with; may be null
    • result

      public CompletionStage<@Nullable String> result()
      Returns:
      the stage that completes when the dialog closes (same as show(limn.scene.Scene))
    • fadeLevel

      public float fadeLevel()
      Returns:
      the current fade level in [0..1] (tests/screenshots)
    • modalWindow

      public @Nullable NativeWindow modalWindow()
      Returns:
      the modal's native window while open (screenshots/tests), else null, which it also is once a modal dialog has moved into the scene of an owner that went fullscreen
    • show

      public CompletionStage<@Nullable String> show(Scene owner)
      Shows the dialog window-modal over owner (locks only its window), or, in DisplayMode.IN_SCENE and over a fullscreen owner, a scene-modal internal overlay.

      The card takes its size step, layout direction and locale from the scene's root, and from the new root when Scene.setRoot(limn.scene.Widget<?>) switches the page while it is open, as every Scene overload of this class does; the Widget overloads take them from the widget given, which stays the card's host whatever happens to the page around it.

      Parameters:
      owner - the scene the dialog is modal over
      Returns:
      a stage that completes with the result when the dialog closes
      Throws:
      IllegalStateException - if this dialog was already shown (dialogs are single-use)
    • showToolkitModal

      public CompletionStage<@Nullable String> showToolkitModal(Scene owner)
      Shows the dialog toolkit-modal over owner (locks every window). In DisplayMode.IN_SCENE, and over a fullscreen owner, it presents the same scene-modal internal overlay as show(limn.scene.Scene), which still locks every other window.
      Parameters:
      owner - the scene the dialog opens over
      Returns:
      a stage that completes with the result when the dialog closes
      Throws:
      IllegalStateException - if this dialog was already shown (dialogs are single-use)
    • showNonModal

      public CompletionStage<@Nullable String> showNonModal(Scene owner)
      Shows the dialog non-modal over owner: a floating window that locks nothing, so the owner (and every other window) stays fully interactive. Only valid in DisplayMode.NATIVE_WINDOW; an in-scene overlay always captures its scene's input. For the same reason it stays a window even over a fullscreen owner, where every other floating surface is drawn in scene, and when its owner goes fullscreen while it is open, and there it pays what a second window costs: an owner fullscreen at a named mode is minimized when the dialog takes the focus, and on a Mac without a GPU the window is slow to open.
      Parameters:
      owner - the scene the dialog opens over
      Returns:
      a stage that completes with the result when the dialog closes
      Throws:
      IllegalStateException - if the display mode is DisplayMode.IN_SCENE, or if this dialog was already shown (dialogs are single-use)
    • show

      public CompletionStage<@Nullable String> show(Widget<?> owner)
      show(Scene) over owner's scene, but inheriting the size step from owner itself rather than from the scene root, the overload to reach for when a SMALL toolbar (or a LARGE settings pane) raises the dialog and the card should match the surface it came from.

      The dialog keeps owner as its host until it closes, even if owner leaves the window meanwhile (taken out of its container, or with the page Scene.setRoot(limn.scene.Widget<?>) replaces): unlike a menu or a popover shown at a widget, it is not closed when the widget goes, since it is a question still waiting for its answer, and its card goes on taking its size step, direction and locale from owner. The widget, and the page around it, therefore stay reachable for as long as the dialog is open. A dialog that should follow the page shown in the window is shown on the Scene instead.

      Parameters:
      owner - the widget whose scene the dialog is modal over, and whose step it takes
      Returns:
      a stage that completes with the result when the dialog closes
      Throws:
      IllegalStateException - if owner is not in a scene, or the dialog was already shown
    • showToolkitModal

      public CompletionStage<@Nullable String> showToolkitModal(Widget<?> owner)
      showToolkitModal(Scene), inheriting the size step from owner, which stays the dialog's host until it closes, as show(Widget) says.
      Parameters:
      owner - the widget whose scene the dialog opens over, and whose step it takes
      Returns:
      a stage that completes with the result when the dialog closes
    • showNonModal

      public CompletionStage<@Nullable String> showNonModal(Widget<?> owner)
      showNonModal(Scene), inheriting the size step from owner, which stays the dialog's host until it closes, as show(Widget) says.
      Parameters:
      owner - the widget whose scene the dialog opens over, and whose step it takes
      Returns:
      a stage that completes with the result when the dialog closes