Class ModalStack

java.lang.Object
limn.backend.spi.ModalStack

public final class ModalStack extends Object
Modality bookkeeping for a backend: a stack of active modals and the decision of whether a given window's input is blocked. Plain Java with no platform in it, so every backend that presents native modals keeps the same rules rather than rewriting them. Windows are opaque identities compared by reference, which lets an in-scene modal, which has no window, be pushed as any token; the parent-to-owned relationship is supplied as a predicate.

Not thread-safe: a backend keeps it on its UI thread, where modals are raised and input is dispatched.

Rules:

  • The topmost modal's surface is never blocked (see below). Every modal under it is.
  • A non-modal window is blocked iff some active modal locks it: a window-modal (non-null parent) locks only its parent (and the parent's owned popups); a toolkit-modal (null parent) locks every non-modal window.
  • A modal may name an owner-exception window it never blocks even when its scope would; in-scene (overlay) modals use it, and their host window stays interactive because the overlay blocks its own content. The exception covers what that host owns as well: a dropdown or a menu is the overlay's own content reaching outside the window to draw itself, not a sibling window the modal is meant to lock. Modals still below the top are unaffected; they are frozen before the exception is consulted.

The top modal is answerable by construction. A modal's surface is the window the user has to reach to dismiss it: itself for a native modal, its host for an in-scene overlay. Exempting that surface is not a convenience; without it the stack can deadlock, and did: a native dialog locks its parent window, the app then raises an in-scene dialog on that parent (an unsaved-changes prompt on the way out is the obvious way in), and now the older modal blocks the newer one's only surface while the newer one blocks the older window. Two open dialogs, neither reachable, and the only way out is killing the process.

  • Constructor Details

    • ModalStack

      public ModalStack()
      An empty stack: nothing is modal and nothing is blocked.
  • Method Details

    • push

      public void push(Object window, @Nullable Object parent)
      Pushes a modal locking parent.
      Parameters:
      window - the modal's own window; never null
      parent - the window it locks, or null for a toolkit-modal that locks every non-modal window
      Throws:
      IllegalStateException - if window is already modal
    • push

      public void push(Object window, @Nullable Object parent, @Nullable Object ownerException)
      Pushes a modal locking parent (null = toolkit-modal) but never locking ownerException nor any window it owns (null = none). This is the shape of an in-scene modal, whose host keeps drawing its own dropdowns and menus as separate windows while the overlay is up.
      Parameters:
      window - the modal's window, or a token standing for an in-scene modal
      parent - the window it locks, or null for a toolkit-modal
      ownerException - the window it never locks, with what that window owns, or null for none
      Throws:
      IllegalStateException - if window is already modal
    • pop

      public void pop(Object window)
      Removes window from the stack; nothing happens if it is not there.
      Parameters:
      window - the modal that closed
    • removeOwnedBy

      public boolean removeOwnedBy(@Nullable Object owner)
      Removes every modal whose owner-exception is owner. It is called when that host window closes, since an in-scene modal has no window of its own to close.
      Parameters:
      owner - the host window that closed
      Returns:
      whether anything was removed
    • active

      public List<ModalStack.Active> active()
      Returns:
      the active modals, the oldest first
    • isEmpty

      public boolean isEmpty()
      Returns:
      whether no modal is active
    • topModal

      public @Nullable Object topModal()
      Returns:
      the topmost (interactive) modal window, or null if none
    • isModal

      public boolean isModal(Object window)
      Parameters:
      window - a window or token
      Returns:
      whether window is one of the active modals
    • isBlocked

      public boolean isBlocked(Object window, BiPredicate<Object,Object> locks, BiPredicate<Object,Object> ownsTransient)
      Parameters:
      window - the window whose input we are deciding on
      locks - locks.test(parent, w) is true iff w is parent or one of its owned popups: the SCOPE a window-modal locks, which covers everything the parent owns
      ownsTransient - ownsTransient.test(host, w) is true iff w is a popup host registered as its own content. Narrower than locks on purpose: it is the EXEMPTION, and exempting everything an in-scene modal's host owns left a non-modal dialog clickable underneath the modal.

      Neither predicate is transitive, and that is a decision. A popup of a popup is not owned by the grandparent. Menus keep a whole cascade inside one window, so the shape does not arise; ModalStackTest asserts it rather than leaving it to be discovered by the first component that spends a window per level.

      Returns:
      whether window's input is currently blocked by a modal