Command handling for embedders
Introduction
The current web browser embedding API used to execute and get state for browser commands,
nsIWebNavigation.idl
,
lists available commands explicitly, with essentially a "do" and "get status" method for each
command (reload, go forward, go back etc.). Interfaces such as this which give the embedder direct
access to execute and query the state of each command allow for interfaces that are
easy to read and implement, but have a number of drawbacks:
- Interfaces get unwieldy for command-rich components such as editor/composer
- No way for the embedded app to notify the embedder when commands need to be updated, so the embedder has to infer update times from other activities, or use an eager update strategy.
The solution is a generic 'DoCommand' and 'IsCommandEnabled' type API, such
as nsICommandHandler
.
Internally, we have interfaces such as
nsIController
(an object which acts as a dispatch point for a set of commands that are handled by a window)
and
nsIControllerCommand
which is an interface via which you can implement a single command in JS or C++ (to be registered
and dispatched by a
nsIControllerCommandManager
).
Requirements
So we need a simple set of interfaces to provide the following functionality for embedders:
- Execute a command via a generic
DoCommand
method - Query the enabled status of a command via a generic
IsCommandEnabled
method - Ask whether a command is supported, via a generic
IsCommandSupported
method - Pass command-specific parameters to the
DoCommand
method (e.g. make bold, set paragraph to be h4) - Query command-specific state of a command (e.g. is the current selection bold?)
- Register and unregister a listener that gets called when one or more commands need to be updated
Interfaces
I propose the following interfaces (names are subject to change if there are conflicts with existing interfaces).
nsICommandManager
An interface through which embedders (and XUL applications) can exectute commands, get command state, and register listeners for command udpates.
GetInterface
(orQI
?) fromnsIWebBrowser
- The embedder may also implement this, to allow for command notifications to
pass from embedded (XUL) chrome out to the embedder. Need some strategy for
QI
ing the emedding container to see if they want this.
[scriptable, uuid()] interface nsICommandManager : nsISupports { void addCommandObserver(in nsIObserver aCommandObserver, in wstring aCommandToObserve); void removeCommandObserver(in nsIObserver aCommandObserver, in wstring aCommandObserved); boolean isCommandSupported(in wstring aCommandName); boolean isCommandEnabled(in wstring aCommandName); wstring getCommandState(in wstring aCommandName); void doCommand(in wstring aCommandName, in wstring aCommandParams); } |
The command state string returned by getCommandState()
reflects
the current state of stateful commands (like Bold, or Paragraph style).
The contents of the string vary depending on the command, and will be documented
on a per-command basis. Some commands (e.g. Back, Reload) don't have corresponding
state, and this call will return an empty string.
Similarly, commands that have state will require a non-empty string in the
aCommandParams
argument to the doCommand()
call.
This will likely one of the state strings returned by getCommandState()
.
Note:
nsICommandHandler.idl
will be obsoleted by these changes.
nsIObserver
Implementation
The nsIObserver
interface is used to notify embedders that commands states have
changed, so that they can change the appearance of applicable UI elements as
appropriate. The observer will be called, usually eagerly, when command state changes,
so implementations should have low overhead. Registering command observers is optional;
UI could just call nsICommandManager::isCommandEnabled()
and nsICommandManager::getCommandState()
on each command it cares about.
The embedder should register an nsIObserver using nsICommandManager::addCommandObserver()
for each command that they wish to get update notifications for. The same observer may be
registered for more than one command. Unregistering on a command does not affect
notifications for other commands on which the observer was registered. To register on all commands
(or, similarly, unregister for all comments on which the observer was registered), pass NULL
or an empty string in the aCommandToObserve
/aCommandObserved
parameter.
Parameters to the Observe()
call on nsIObserver
for
a command update notification:
[scriptable, uuid()] interface nsIObserver : nsISupports { void Observe( in nsISupports aSubject, // The nsICommandManager calling this Observer in wstring aTopic, // Name of the command being updated in wstring someData ); // Command state (if available) }; |
Note: the command state passed as the last argument to Observe()
may
be expensive to compute, and often the embedder won't care. Maybe not send this, so the
embedder must call nsICommandManager::getCommandState()
if they need it?
How to make or get one:
- Embedder implements this interface.
Implementation notes
Currently we have a command dispatcher for xul, the
nsIDOMXULCommandDispatcher
,
which manages command updater objects (XUL document nodes that have the
commandupdater
attribute set to "true
", and a list
of nsIControllers
,
which handle individual commands.
This command dispatcher needs to be made independent of XUL. Its focus-related duties
have already been moved into nsIFocusController
, and what remains can
be morphed into an implementation of the nsICommandManager
described above.
Once that is done, the various places in the JS (and C++) that look like this
(note that the isCommandEnabled()
call is not strictly necessary here, but is
included for illustration):
var controller = document.commandDispatcher.getControllerForCommand("cmd_copy"); if (controller.isCommandEnabled("cmd_copy") controller.doCommand("cmd_copy"); |
can be changed to call the new nsICommandManager
:
if (window.commandManager.isCommandEnabled("cmd_copy")) window.commandManager.doCommand("cmd_copy"); |
Questions:
- What about the cool
nsIControllerCommand
/nsIControllerCommandManager
that allows you to write an individual command as an object that supports thensIControllerCommand
interface? Should this be exposed to embedders? Should all commands someday be written this way?
Simon Fraser