Package com.iizix.swt

Class BrowserBoot

java.lang.Object
com.iizix.swt.BrowserBoot

public class BrowserBoot extends Object
Boots an SWT Browser through its load + readiness + retry lifecycle.

The SWT Browser widget on the Edge/WebView2 backend (Windows 11) does not finish initializing until its control hierarchy is actually visible on screen. A setUrl (or setText) issued before that moment can be silently dropped: the page never loads, the injected BrowserFunction bridge never becomes live, and any JavaScript→Java readiness callback the site waits for never fires. The browser cell sits blank. Re-issuing the navigation (a manual "Reload") always fixes it, because by then the control is visible.

BrowserBoot centralizes that lifecycle: it owns the readiness handshake, the gated/retrying load, the file: URL pre-validation, and (via the create(org.eclipse.swt.widgets.Composite) factory) creation-failure handling. The site continues to register its own other BrowserFunctions directly on the browser; BrowserBoot does not wrap or manage those.

Two distinct readiness moments are exposed. Each may be supplied either as a Consumer<Browser> (browser only) or as a BiConsumer<Browser,Object[]> (browser plus the readiness call's JavaScript arguments); both forms are nullable:

  • onBridge — fires when the readiness BrowserFunction is called from the page (JS→Java confirmed). Most sites pass null here.
  • onReady — by default fires once, on the UI thread, when the browser is REALLY ready, meaning BOTH the readiness BrowserFunction has fired (JS→Java works) AND ProgressListener.completed(org.eclipse.swt.browser.ProgressEvent) has fired (Java→JS into the loaded page works). In refire mode it fires on each subsequent bridge call too (see below).

The readiness signal reaches BrowserBoot one of two ways. Either BrowserBoot owns the BrowserFunction (register it with readyFunction(java.lang.String)), or the site owns a BrowserFunction of its own and calls markBridgeFired() from inside it. The latter is required when the site's readiness function is overloaded — called both as the bare handshake and, separately, to return data (e.g. a computed size) — because a site-owned function can demultiplex by argument count whereas a BrowserBoot-owned one cannot. When the BiConsumer forms are used, the arguments from the bridge call are stashed and replayed into onReady, so onReady always sees the bridge's arguments regardless of whether the bridge or completed fired last.

REFIRE MODE (opt-in, enableReadyRefire(boolean)): by default onReady fires exactly once. Refire mode makes it fire on each subsequent readiness-bridge call instead, for a STABLE page whose readiness function is deliberately called more than once (e.g. an overloaded onJSInitialized that returns a computed size on a later call). The mode imposes a page contract: the page MUST NOT be re-loaded (a second load(java.io.File)/loadText(java.lang.String) throws IllegalStateException while refire mode is on) and MUST NOT navigate (a real in-page navigation automatically disables refire mode, resets the readiness state, and degrades the instance to fire-once). A completed event alone never re-fires onReady; only a bridge call does.

CRITICAL: the retry (re-issuing the load) is gated on the readiness BrowserFunction having fired ONLY, never on completed. The original hang was the SWT BrowserFunctions never being injected into the page at all; in that hang completed can still fire (the document loaded) while the bridge is dead, so gating retry on completed would fail to retry the actual hang. The BrowserFunction firing proves injection succeeded, which is the correct retry-stop signal.

All callbacks run on the SWT UI thread (BrowserFunction callbacks and timerExec are UI-thread by SWT contract), so the per-instance state needs no synchronization.

Author:
Christopher Mindus
  • Method Details

    • create

      public static BrowserBoot create(Composite parent)
      Creates a browser via SWTHelper.createBrowserOrCompositeForFailure(org.eclipse.swt.widgets.Composite, org.eclipse.swt.graphics.Color) and wraps it.

      The factory ALWAYS returns a BrowserBoot (never null). When browser creation fails (e.g. Windows GDI/USER handle exhaustion) the wrapped component is a read-only fallback Composite showing the exception, and isBrowser() returns false. The caller lays out getComponent() (the browser OR the error UI) and only calls load(java.io.File) when isBrowser() is true.

      Parameters:
      parent - The parent composite.
      Returns:
      A new BrowserBoot, never null.
    • create

      public static BrowserBoot create(Composite parent, Color background)
      Creates a browser via SWTHelper.createBrowserOrCompositeForFailure(org.eclipse.swt.widgets.Composite, org.eclipse.swt.graphics.Color) and wraps it.

      The factory ALWAYS returns a BrowserBoot (never null). When browser creation fails (e.g. Windows GDI/USER handle exhaustion) the wrapped component is a read-only fallback Composite showing the exception, and isBrowser() returns false. The caller lays out getComponent() (the browser OR the error UI) and only calls load(java.io.File) when isBrowser() is true.

      Parameters:
      parent - The parent composite.
      background - The background color, nullable.
      Returns:
      A new BrowserBoot, never null.
    • create

      public static BrowserBoot create(Composite parent, Color background, Consumer<Browser> onBridge, Consumer<Browser> onReady)
      Creates a browser via SWTHelper.createBrowserOrCompositeForFailure(org.eclipse.swt.widgets.Composite, org.eclipse.swt.graphics.Color) and wraps it.

      The factory ALWAYS returns a BrowserBoot (never null). When browser creation fails (e.g. Windows GDI/USER handle exhaustion) the wrapped component is a read-only fallback Composite showing the exception, and isBrowser() returns false. The caller lays out getComponent() (the browser OR the error UI) and only calls load(java.io.File) when isBrowser() is true. onReady only ever fires when there IS a browser.

      Parameters:
      parent - The parent composite.
      background - The background color, nullable.
      onBridge - Fires when the readiness function fires; nullable.
      onReady - Fires once when really ready; nullable.
      Returns:
      A new BrowserBoot, never null.
    • create

      public static BrowserBoot create(Composite parent, Color background, BiConsumer<Browser,Object[]> onBridgeParams, BiConsumer<Browser,Object[]> onReadyParams)
      Creates a browser via SWTHelper.createBrowserOrCompositeForFailure(org.eclipse.swt.widgets.Composite, org.eclipse.swt.graphics.Color) and wraps it.

      The factory ALWAYS returns a BrowserBoot (never null). When browser creation fails (e.g. Windows GDI/USER handle exhaustion) the wrapped component is a read-only fallback Composite showing the exception, and isBrowser() returns false. The caller lays out getComponent() (the browser OR the error UI) and only calls load(java.io.File) when isBrowser() is true. onReady only ever fires when there IS a browser.

      Parameters:
      parent - The parent composite.
      background - The background color, nullable.
      onBridgeParams - Fires when the readiness function fires, with that call's arguments; nullable.
      onReadyParams - Fires once when really ready, with the bridge call's arguments; nullable.
      Returns:
      A new BrowserBoot, never null.
    • attach

      public static BrowserBoot attach(Browser browser)
      Wraps an already-created Browser (escape hatch for sites that create and manage their own browser, e.g. the tooltip's measure-then-show flow), with no callbacks set.

      This is the no-callback overload, for sites that drive readiness entirely through a site-owned BrowserFunction (gating the retry with markBridgeFired()) and therefore need neither onBridge nor onReady. It exists to avoid an ambiguous (null,null) call between the Consumer and BiConsumer overloads, and the casts that would otherwise be required. Callbacks may still be set afterwards via onBridge/onReady.

      Parameters:
      browser - The browser to drive; not null.
      Returns:
      A new BrowserBoot, never null.
    • attach

      public static BrowserBoot attach(Browser browser, Consumer<Browser> onBridge, Consumer<Browser> onReady)
      Wraps an already-created Browser (escape hatch for sites that create and manage their own browser, e.g. the tooltip's measure-then-show flow).
      Parameters:
      browser - The browser to drive; not null.
      onBridge - Fires when the readiness function fires; nullable.
      onReady - Fires once when really ready; nullable.
      Returns:
      A new BrowserBoot, never null.
    • attach

      public static BrowserBoot attach(Browser browser, BiConsumer<Browser,Object[]> onBridgeParams, BiConsumer<Browser,Object[]> onReadyParams)
      Wraps an already-created Browser (escape hatch for sites that create and manage their own browser, e.g. the tooltip's measure-then-show flow).
      Parameters:
      browser - The browser to drive; not null.
      onBridgeParams - Fires when the readiness function fires, with that call's arguments; nullable.
      onReadyParams - Fires once when really ready, with the bridge call's arguments; nullable.
      Returns:
      A new BrowserBoot, never null.
    • isBrowser

      public boolean isBrowser()
      Returns whether a real Browser was created.
      Returns:
      true if a Browser is present, false if creation failed (fallback Composite).
    • getBrowser

      public Browser getBrowser()
      Returns the Browser, or null if creation failed.
      Returns:
      The Browser, or null.
    • getComponent

      public Composite getComponent()
      Returns what is actually in the layout: the Browser, or the fallback error Composite. Never null. The caller sets layout data on THIS.
      Returns:
      The component to lay out, never null.
    • readyFunction

      public BrowserBoot readyFunction(String name)
      Registers the readiness BrowserFunction — the JS→Java half of the handshake.

      When the page calls this function (from its DOM-ready/init handler), BrowserBoot marks the bridge as fired, invokes onBridge (if non-null) and, if completed has also fired, latches ready and runs onReady (once, or repeatedly in refire mode; see enableReadyRefire(boolean)). The name varies by site — pass the exact string the page JS calls (e.g. "onJSInitialized").

      If browser creation failed (no browser), this is a silent no-op: creation failure is a designed, queryable state (see isBrowser()), not an error. A disposed browser or a second registration are programming errors and throw IllegalStateException.

      The readiness function is registered ONCE for the lifetime of this BrowserBoot; a second call always throws. Sites whose readiness function must be site-owned (e.g. an overloaded function that also returns data) do not call this at all — they call markBridgeFired() instead.

      Parameters:
      name - The readiness function name as called by the page.
      Returns:
      This BrowserBoot, for chaining.
      Throws:
      IllegalStateException - if the browser is disposed, or the readiness function has already been registered.
    • onBridge

      public BrowserBoot onBridge(Consumer<Browser> onBridge)
      Sets the onBridge callback (alternative to passing it to create(org.eclipse.swt.widgets.Composite)/attach(org.eclipse.swt.browser.Browser)). Any previous onBridge settings are removed in this call.
      Parameters:
      onBridge - Fires when the readiness function fires; nullable.
      Returns:
      This BrowserBoot, for chaining.
    • onBridge

      public BrowserBoot onBridge(BiConsumer<Browser,Object[]> onBridgeParams)
      Sets the onBridge callback (alternative to passing it to create(org.eclipse.swt.widgets.Composite)/attach(org.eclipse.swt.browser.Browser)). Any previous onBridge settings are removed in this call.
      Parameters:
      onBridgeParams - Fires when the readiness function fires, with that call's arguments; nullable.
      Returns:
      This BrowserBoot, for chaining.
    • onReady

      public BrowserBoot onReady(Consumer<Browser> onReady)
      Sets the onReady callback (alternative to passing it to create(org.eclipse.swt.widgets.Composite)/attach(org.eclipse.swt.browser.Browser)). Any previous onReady settings are removed in this call.
      Parameters:
      onReady - Fires when really ready (once by default; repeatedly in refire mode); nullable.
      Returns:
      This BrowserBoot, for chaining.
    • onReady

      public BrowserBoot onReady(BiConsumer<Browser,Object[]> onReadyParams)
      Sets the onReady callback (alternative to passing it to create(org.eclipse.swt.widgets.Composite)/attach(org.eclipse.swt.browser.Browser)). Any previous onReady settings are removed in this call.
      Parameters:
      onReadyParams - Fires when really ready (once by default; repeatedly in refire mode), with the bridge call's arguments; nullable.
      Returns:
      This BrowserBoot, for chaining.
    • enableReadyRefire

      public BrowserBoot enableReadyRefire(boolean on)
      Enables or disables REFIRE MODE.

      By default onReady fires exactly once, when the browser is really ready. In refire mode it instead fires on EACH subsequent readiness-bridge call (a repeat call to the readyFunction(java.lang.String), or a repeat markBridgeFired()), carrying that call's arguments. This is for a STABLE page whose readiness function is called repeatedly by design — e.g. an overloaded onJSInitialized that signals init once and is then called again to return a computed size. A completed event alone never re-fires onReady; only a bridge call does.

      Refire mode imposes a contract on the page (see the class documentation):

      The flag is wiring, not per-cycle state: it is not cleared by a (legal) load. It may be toggled at any time.

      Parameters:
      on - true to enable refire mode, false to restore fire-once behavior.
      Returns:
      This BrowserBoot, for chaining.
    • markBridgeFired

      public void markBridgeFired()
      Marks the JS→Java bridge as fired from outside, for sites whose readiness signal is a site-owned BrowserFunction rather than a BrowserBoot-registered readyFunction(java.lang.String).

      This is the alternative to readyFunction(java.lang.String) for sites that, for whatever reason, keep ownership of the readiness BrowserFunction themselves (e.g. to demultiplex an overloaded function by argument count entirely in their own code). The site calls this from inside its own function to gate the retry: it sets bridgeFired, invokes onBridge (if non-null) and, if completed has also fired, latches ready and runs onReady (once, or repeatedly in refire mode) — exactly as a registered readiness function would.

      NOTE: an overloaded readiness function (called once bare for init, then again with data such as a computed size) does NOT require this method. It can be registered normally with readyFunction(java.lang.String) and the repeat calls delivered through refire mode; the font-preview tooltips do exactly that. markBridgeFired is only needed when the site must own the BrowserFunction for some other reason.

      This no-argument form passes null to any BiConsumer callbacks. To forward the site function's own arguments, use markBridgeFired(Object[]). Must be called on the UI thread.

    • markBridgeFired

      public void markBridgeFired(Object[] params)
      Marks the JS→Java bridge as fired from outside, carrying the arguments the site-owned BrowserFunction received. Same as markBridgeFired() but the supplied params are passed to onBridge/onReady BiConsumer callbacks and stashed for the onReady replay. Must be called on the UI thread.
      Parameters:
      params - The site function's arguments, nullable.
    • loadWebRoot

      public void loadWebRoot(String path)
      This method loads a file relative the web server document root. Issues a gated, validated, retrying setUrl.

      If the file does not exist, an InternalError is thrown. No-op if there is no browser.

      Parameters:
      path - The file path with '/' as directory separators, a file relative the web server root.
      Throws:
      InternalError - if the file or the web server root cannot be found.
      IllegalStateException - if refire mode is on and a load has already been issued (the single-load contract; see enableReadyRefire(boolean)).
    • load

      public void load(File file) throws MalformedURLException
      Issues a gated, validated, retrying setUrl.

      The file URL the target is pre-validated: if the file is confirmed missing/unreadable, a clear error is logged and the retry loop is NOT entered (retrying a non-existent file is pointless). If the URL cannot be parsed to a file, the pre-check fails open and the load proceeds. Non-file schemes (http/https/about/data) are never pre-checked — a transient (e.g. a still-starting localhost server) is exactly what the retry is for. No-op if there is no browser.

      Parameters:
      file - The file to load.
      Throws:
      IllegalStateException - if refire mode is on and a load has already been issued (the single-load contract; see enableReadyRefire(boolean)).
      MalformedURLException - if the File cannot be converted to an URL due to malformed .toURL() called on file.toURI().
    • load

      public void load(URL url)
      Issues a gated, validated, retrying setUrl.

      For a file: URL the target is pre-validated: if the file is confirmed missing/unreadable, a clear error is logged and the retry loop is NOT entered (retrying a non-existent file is pointless). If the URL cannot be parsed to a file, the pre-check fails open and the load proceeds. Non-file schemes (http/https/about/data) are never pre-checked — a transient (e.g. a still-starting localhost server) is exactly what the retry is for. No-op if there is no browser.

      Parameters:
      url - The URL to load.
      Throws:
      IllegalStateException - if refire mode is on and a load has already been issued (the single-load contract; see enableReadyRefire(boolean)).
    • load

      public void load(String url)
      Issues a gated, validated, retrying setUrl.

      For a file: URL the target is pre-validated: if the file is confirmed missing/unreadable, a clear error is logged and the retry loop is NOT entered (retrying a non-existent file is pointless). If the URL cannot be parsed to a file, the pre-check fails open and the load proceeds. Non-file schemes (http/https/about/data) are never pre-checked — a transient (e.g. a still-starting localhost server) is exactly what the retry is for. No-op if there is no browser.

      Parameters:
      url - The URL to load.
      Throws:
      IllegalStateException - if refire mode is on and a load has already been issued (the single-load contract; see enableReadyRefire(boolean)).
    • loadText

      public void loadText(String html)
      Issues a gated, retrying setText. Same as load(java.io.File) but with HTML and no file pre-check. No-op if there is no browser.
      Parameters:
      html - The HTML to load.
      Throws:
      IllegalStateException - if refire mode is on and a load has already been issued (the single-load contract; see enableReadyRefire(boolean)).
    • setBackgroundColor

      public boolean setBackgroundColor(Color c)
      Sets the browser's background color on the HTML body. A null color is a no-op. In case of a browser execution error, a severe event is logged (see runJS(java.lang.String)).
      Parameters:
      c - The color, or null for no operation.
      Returns:
      true on success or when c is null (no-op); false if the script failed.
    • setForegroundColor

      public boolean setForegroundColor(Color c)
      Sets the browser's foreground (text) color on the HTML body. A null color is a no-op. In case of a browser execution error, a severe event is logged (see runJS(java.lang.String)).
      Parameters:
      c - The color, or null for no operation.
      Returns:
      true on success or when c is null (no-op); false if the script failed.
    • runJS

      public boolean runJS(String script)
      Runs a script in the SWT Browser using browser.evaluate(script). In case of a browser execution error, a severe event is logged and false is returned.

      evaluate is used rather than execute because it is what most of IIZI uses, and because it raises a SWTException on a script error (which we catch and log) instead of silently returning a flag. NOTE: this only catches errors raised synchronously by the script; an exception thrown later inside an asynchronous callback the script schedules (e.g. a setTimeout) cannot be observed here.

      Parameters:
      script - The JavaScript string.
      Returns:
      true on success; false if there is no (live) browser, or the script raised an error.