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 abandonedsome 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.
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:
window.open
.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.
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.
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.
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.
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.
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
nsIDOMWindow *aParent
parameter mentioned above
should be the first parameter to each method in which it appears.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.)
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.
Window Watcher
.xpfe
, into a
component more likely to be included in an embedding
distribution.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:)
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.
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:
nsIWindowCreator
, and let the Window Watcher hold on to
it by calling nsIWindowWatcher::SetWindowCreator
.nsIWebBrowserChrome
must be able to cough up an
nsIDOMWindow
in its
nsIInterfaceRequestor::GetInterface
method.Implemented. It's the nsIWindowWatcher
component.
activeWindow
attribute (by setting it when the
active window changes), though truth be told, in the current codebase, the
attribute is never used.
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.
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.