A somewhat detailed explanation of tasks necessary to fix Gecko and supporting components to be able to pose dialogs while running as an embedded web browser. Here's a basic list of tasks, followed by what details I know at the moment.

20 Mar 01
author

Change history

4 Apr 01 Only complex components need support piecemeal overriding of dialogs.

13 Mar 01 Largely rewritten; updated to show current status

20 Mar 01 Included conclusions of the embedding API review. Added programming notes.

New work

Work finished or abandoned

Dialog Posing API

some investigation, some new code, some grunge.

This section describes changes we need to make to code which poses dialogs to ready it for use as an embeddable browser. It begins with history and explanation, but hurried programmers may want to skip ahead to the programming notes section.

It seems generally accepted that embedding apps need to be able to request that Gecko pose one of its dialogs, and that they be able to replace any of Gecko's dialogs with their own. Note that in this Dialog Posing Section, an embedded context is the same as an installation context, meaning that the problem and solution are the same for an application wishing to embed a Gecko browser, and for a customized installation of the Mozilla browser.

To pose a dialog, Mozilla code generally simply calls window.open (or the C++ equivalent) directly. We plan to substitute dialog-posing components which can be swapped out by emdedder's components. A few scattered groups have already begun to architect their UIs this way; we're just building on it and canonizing it.

An earlier plan was to roll all dialog posing methods into a single generic interface. We have abandoned the generic thrust and now plan a collection of individual components. Code that needs to present one or several dialogs and could be called from an embedded Gecko browser must collect its dialog posing code into a special-purpose interface, built into a component. The dialog posing interface for each component is free to be unique to that component, since we've abandoned as impractical the effort to standardize this thing. (Though see a restriction under Dialog Parentage).

We are explicitly omitting dialogs which wouldn't appear in an embedded browser. For example Composer's Image Properties Dialog isn't included, but the nsIPrompt alerts are. One of our tasks in coding this will be to make an exhaustive list of all affected dialogs. But this document includes a preliminary list of likely dialog-serving components, presented as a guide, not a mandate.

The point behind all this work is to make all dialogs pluggable. An embedding application will have detailed knowledge on which dialogs can be posed by an embedded browser, and can at its discretion override any or all of them. To replace a dialog by one of its own; that is, to force Gecko to display a dialog of their construction rather than Mozilla's, the embedding application must install its own version of the dialog-serving component. Embedded Gecko (or the full Mozilla app) will preferentially use that component when it runs into code that uses one of these dialog-serving APIs to pose a dialog. This goal implies a few additional architectural changes, described in the remainder of this section.

Architectural Changes

Dialog Parentage

Dialogs generally must be attached to their parent window in some fashion, to appease OSes with window-modal dialogs (as opposed to application-modal). Currently the Mozilla codebase addresses this in one of three ways:

This document doesn't address the third case, which luckily is relatively rare. The first two cases must adapt under this plan. The solution isn't as complex as the problem's description. Each method which poses a dialog in a component's interface must take an nsIDOMWindow parent parameter. The objects currently used to parent dialogs in the first two cases above can readily cough up an nsIDOMWindow, so we shouldn't need to add to the list of dialogs in the third category.

Data Transfer Methods

While some dialogs are posed solely from within the browser, others are posed directly by the owning application. Preferences, for instance. For the embedding application to write its own interface, these dialogs must provide, in addition to methods to pose the dialog, methods to populate the dialog from Gecko data and methods to push changed data from the dialog back into Gecko. Preferences, for instance, has nsIPref. Other such dialogs will need similar access.

Piecemeal Overriding of Dialogs

Some of the dialog UI components may be large and complex. The people behind this document have nevertheless agreed that it will be acceptable if, to override one dialog, an embedding application or installation must write its own implementation of the complete interface. However, we believe a scheme for overriding only certain methods within a component is desirable and way more slick, and we spec a simple way for complex UI components to be able to support this capability.

Regardless of whether an individual component chooses to support this capability, all dialog-posing code must be prepared for the existence of two components implementing each UI-posing interface: the default Mozilla version and an embedding application's override version. That constrains dialog-posing code to never pose dialogs directly, but always go through the correct UI component. Additionally, complex UI components should allow an embedding app to override only a subset of all their dialogs, as outlined in the guidelines section.

There still remains the issue of whether we will support possible multiple applications each sharing a single instance of a Mozilla installation. While there may be issues preventing that, the dialog posing scheme outlined here should be able to handle it by enforcing a convention that, while the default Mozilla implementations may be automatically registered components, the application override implementations must be individually registered with the component manager at startup each time the application launches, and that registration is not to be persistent across subsequent launches of the application.

Preliminary list of dialog posing components

Dialog Posing Tasks

  1. Build a categorized list of every Mozilla dialog affected by this plan. (The categories include whether a dialog is affected, which component it should live in, and whether additional data transfer methods will be required.)
  2. Collect the above dialogs into components
  3. Change all places in the code which currently pose these dialogs directly to instead go through the (redirectable) component
  4. Retrofit nsCommonDialogs into this scheme and rework the current nsIPrompt scheme as a service like any other (low priority, because the current scheme is working though soon to be inconsistent with the rest of the product)
  5. documentation including programmer's guidelines

Programming Notes

Dialogs posed by the embedded browser ("up calls")

Any component which may be included in an embedded browser distribution and wishes to pose a dialog must group its UI (code which runs its dialogs) into a unique interface built just for that purpose. Any such code which isn't already a component needs to be. All code which actually poses dialogs, if it does this directly using window.open, wants to be changed to go through the new interface.

Note that nsIPrompt dialogs will be one such component, so a component whose UI solely uses nsIPrompt is already up to spec.

Dialogs posed by the embedding app ("down calls")

Some components will not pose dialogs directly, but maintain a database of information an embedding app may wish to display as a dialog. Preferences are the canonical example. In this case, we must be sure to have an API which an embedding app can use to populate its own dialog, should it decide to eschew Mozilla's XUL version. These components need build no dialog posing interface.

Guidelines

A few restrictions on each component's UI interface (yes I know...). Most if not all of these dialogs will need to be child/dependent/transient windows, so each method responsible for actually opening a window must take an nsIDOMWindow *aParent parameter, and use that window as the dialog's parent, if non-null.

In some cases, we want to allow an overriding implementation to be able to implement only certain dialogs within a component, rather than being forced to implement them all. Most of the burden is on the embedding application, but a Gecko dialog-posing component needs to do a little extra work.

Not all components need to worry about this. The benefit of supporting this capability is kind of unremarkable for a small, simple component. Complex components for which this capability makes sense (we leave this to the judgment of the component author!) should go to a little extra effort to allow an overriding implementation to override it piecemeal.

A complex dialog-posing component should register itself with two Contract IDs, both referring to the same interface and implementation. Both will be published as part of Gecko's embedding API, though one is "public" and one "private". (Really, "basic" and "overrideable".) All UI components may be overridden by an embedding application or peculiar installation which may, at its discretion, register its own version of each UI component with the Component Manager at startup. Within complex components, we would also like for them to be able to override single dialogs. The scheme is for the default Gecko components to respond equally to two Contract IDs, and for the overriding component (if any) to override only the overrideable version. The overriding component will include its own implementation for each method it wishes to override, and it will be responsible for calling through to the base Gecko interface for each method it wishes to leave alone.

To make our embedding API as consistent as possible, we'd like to impose a few coding conventions on these dialog-posing interfaces. We'll probably add more in time, but for now, here's the complete list:

Embedded dialog posing interface coding conventions

We wish we had actual code examples. The idea is straightforward; hopefully that will be enough for now, as we're just getting started. PSM 2 is headed this way. You may want to look in mozilla/security/manager/pki for ideas.

We have been building a list of affected components, currently arranged as a series of approximations converging on a final list; attachments to Bugzilla bug 71837. If you know of omissions or unnecessary inclusions, please let us know.)

Sanctify nsIPromptService

new code

It seems that for practical reasons we'll never successfully fit a window context into all the places that initiate a channel. (See the issue of Dialog Parentage.) So rather than try to force the issue, the plan is to make nsIPromptService work in an embedding context. Standing in the way is nsIPromptService's use of the Evil Hidden Window; an object present only in the Mozilla application. It uses Hidden Window to open a new window (because before the recent ability to open windows without a parent, windows couldn't be created in the absence of an extant window to serve as their parent). But we should now be able to fix nsIPromptService to work in an embedded context. Windows created through this mechanism will still misbehave: they'll fail to be modal to their parent (important on platforms where modal windows are window modal, not application modal). That's unfortunate, but we have no solution at this time.

nsIPromptService sanctification tasks

  1. Replace usage of Hidden Window in nsCommonDialogs with the Window Watcher.
  2. Move the component containing it out of xpfe, into a component more likely to be included in an embedding distribution.

The Event Loop

documentation

The issue here is how the embedding app and Gecko will cooperate to implement modality, and what repercussions this will have on both. A modal dialog must do two unique things (ooo, a list:)

  1. lock out user interaction with other windows (or at least the parent window, depending on the platform)
  2. prevent the calling code from continuing until the dialog is dismissed, without losing important events

People seem to be agreeing that we need not worry about the modal event loop in an embedded Mozilla, for reasons spelled out in an older version of this document. Basically it becomes an absurd exercise for Gecko to even try to provide a workable captive event loop suitable for use in foreign applications. Our current chosen direction is one where the application provides its own support for modality, and we must merely document this.

Note that this makes the task of showing an alert (like nsIPrompt::Alert()) potentially more complicated to the calling application than merely calling the function. That's unfortunate, but I think our options are limited. Note also that nsIPrompt functions may not be at issue, if they are implemented by the embedding app as a naturally modal system dialog (like Windows' MessageBox), as entertained in the nsIPrompt section. This decision may affect only other, rarer dialogs which the embedding application may want to be modal.

event loop tasks

  1. document unusual Gecko requirements for posing modal dialogs in an embedded environment.
Tasks from the previous version of this document, now implemented or abandoned:

Open Windows Without an nsXULWindow

Implemented. The new nsIWindowWatcher service contains a public method, OpenWindow. This is the supported way to create a window without a parent (by passing null in the aParent parameter).

For this to work, the owning application must conform to two requirements:

Window Mediator Component

Implemented. It's the nsIWindowWatcher component.

This service generally requires no new maintenance from embedding applications; it's hooked in at a Mozilla code level. Embedding applications are required to maintain its activeWindow attribute (by setting it when the active window changes), though truth be told, in the current codebase, the attribute is never used.

Augment nsIPrompt

Current status: while the points below remain a concern, we've decided to generally leave nsIPrompt alone, except its implementation, as discussed in the dialog posing API section.

All dialogs should be children of some owning window. Otherwise, at best there can be confusion about which browser window (for example) a dialog belongs to. At worst a modal alert will be unable to lock out user interaction with the parent window. The worst case causes programmer surprises and occasional crashes. We must be able to retrieve a suitable parent window any time we wish to pose a dialog.

Two possible schemes for accomplishing this have been raised. These are: an augmentation to our current scheme in which references to the top-level window are carried around in contexts which may conceivably want to pose a dialog, and a global service which can be trusted to always hold a reference to the window corresponding to the network access currently being processed. These were both discussed in an earlier version of this document. In this one I'm running with the conclusion, which was that we stick with our current nsIPrompt scheme. Both schemes have their hoary undersides. Suffice to say that even the more general sounding other solution doesn't work in all situations which already occur in the codebase.

Mozilla's current standard way of knowing the correct parent for a dialog relies on the code which initiates a networking connection (at some early point, every operation can locate its correct window) to stash a reference to the top-level window, disguised as an nsIInterfaceRequestor, in the channel. All current uses of this object QI it to an nsIPrompt. This holds for both Mozilla-the-app and for our embedding windows, as currently specified.

The fact that all top-level windows, both Mozilla and embedding, already must implement nsIPrompt takes care of our immediate problem of moving nsIPrompt into the embedding world. Arguably, we're done with that (though I'll propose an extension requested by some outside developers, but that isn't immediately necessary.) What's left is the ability to pose a few more complicated dialogs. There's a move on to simplify nsIPrompt, to make the task of implementing this basic UI more straightforward for embedding apps. This may end up being a bootless simplification because of modifications described in the dialog posing API section.

Exorcise nsIPromptService

Current status: while the points below remain a concern, we've switched our tack and decided instead to Sanctify nsIPromptService.

In the nsIPrompt soliloquy I mentioned that current accepted behaviour is to stash an nsIPrompt in each networking access' channel, from where it can be retrieved by code which does not naturally belong to a window but wishes to pose a dialog (like network error alerts). This is done in nearly all cases, but it's still wise to code a fallback in the dialog posing code.

The dialog posing code attempts to retrieve an nsIPrompt from the nsIChannel. Failing that, it uses the global nsIPromptService. That creates a window whose parent is Hidden Window. It nearly works in the application, but the alert is incorrectly parented and behaves badly (on systems for which modal windows are modal only to their parent, it neglects to lock out user interaction with the parent window, leading to possible crashes). And it doesn't work in the embedding case, since there is no Hidden Window there.

It's important for this scheme to work that each piece of code that initiates a channel hook it up with a proper nsIPrompt. Unfortunately, each such place in the code is rather unique. Most do; a few do not. A typical, or at least likely case, is the first load that happens when a browser window begins loading a web page. That load is fired by an nsDocShell. The nsDocShell has access to the nsIPrompt corresponding to its top-level window, and happily hands that along as an nsIPrompt. That one's relatively easy.

The Channel doesn't carry an nsIPrompt directly; it contains an nsIInterfaceRequestor, accessible through its GetNotificationCallbacks method. If the channel's creator was thoughtful enough to put an nsIPrompt in that nsIInterfaceRequestor slot, you can request an nsIPrompt from that, and this is the common pattern in Mozilla. Occasionally the initiator doesn't put an nsIPrompt in the channel, but something else instead (last I looked, imglib was doing this somewhere). That causes problems in the current code and wants to be fixed. We need to do something like ... urk ... teach the channel to hold a list of notification callbacks and cycle through them each time.

tasks to exorcise nsIPromptService

  1. Teach our nsChannel to be able to hold more than one nsIInterfaceRequestor, and its users to be able to deal with more than one.
  2. Locate all places where a channel is initiated. Most already associate an nsIPrompt with the channel. Those that don't. should also add an nsIPrompt.
  3. Remove the global service fallbacks relied on by distrustful end consumers.
  4. Remove the global service.