[PEAK] Menus and toolbars and plugins, oh my...
Phillip J. Eby
pje at telecommunity.com
Tue Nov 6 19:36:20 EST 2007
So I'm working on trellis-ized UI stuff, and I'm seeing some patterns
with menus and toolbars that need to be extensible via plugins.
Both menus and toolbars are sequences of possibly-grouped commands,
possibly with "checked-ness" or "radio-ness", and possibly
hierarchical. Plugins need to be able to insert (localizable)
commands (or groups thereof) into existing groups, which means they
need to be able to refer to existing groups and commands. Separators
can be placed in menus and toolbars to partition command groups,
depending on the nature of the group. (E.g. a submenu group vs. a
radio item group.) We also need some degree of GUI toolkit
independence, and skinning or styling.
This message is a set of notes groping towards the specification of a
system that handles all this, preferably in such a way that it can
also handle Eclipse-style "activities" (groups of features that can
be added or removed from the UI), "contexts" (in which particular
commands are bound to specific keystrokes), and "schemes" (that
define standardized keystroke sets, e.g. "Emacs-like").
Oh yeah, and it should do all this in such a way that the basic
framework doesn't need all of this to be implemented at once, so it
doesn't take forever to write it. :)
Ordering
--------
Relative placement can probably be achieved by allowing command
entries to refer to items they should "place_before" or
"place_after". The same thing can be done with groups. In addition,
the relative order of two commands or groups with the same placement
preferences should be *stable*. That is, if A and B want to be
placed after C, then A and B should always be in the same relative
order to each other, no matter what other plugins may add additional
items between, before, or after them.
A relatively simple way to do this, would be to allow entries to have
a sequence of constraint pairs (where each pair is a direction and a
target), in priority order. When placing an item, the first
constraint can always be satisfied, but multiple constraints may not
be satisfiable.
So, to create the order, you first need some stable ordering of the
plugins contributing items. And, going through the plugins in that
order, you attempt to add their items, looping over their
constraints. The first constraint is simply applied by inserting the
item at the specified position. Subsequent constraints are then
applied one by one until a previous constraint breaks, at which point
the placement is left alone.
In the common case, of course, most items will have only one or two
constraints. It should also be possible to have a constraint target
be the containing group, so that you can specify "before the group"
or "after the group", to indicate a desire to be the first-most or
last-most (with last-most being the default if no ordering is
otherwise specified).
For the sake of simplicity, it should also be possible to just create
a group of commands in list form, and have the appropriate
before/after constraints automatically added to each one.
References
----------
For a plugin to insert items, it must be able to reference existing
group(s) in which to place them, and existing item(s) or group(s) to
place them relative to. This leads to some interesting questions as
to how to do this referencing.
The simplest way in many respects would be to use direct object
references; that is, simply importing the command groups or menus
from the modules that define them. There are, however, a couple of
downsides to this approach:
1. The items must be importable from a *stable* location/name --
otherwise, changes will break the plugins.
2. If multiple plugins create, say, a "Tools" menu, there will be two
distinct objects -- and thus two "Tools" menus!
These problems are actually more related than they seem at first
glance. If it's possible for a group to be missing (i.e.
unimportable), then a plugin needs to be able to redundantly define
the group's properties/ordering etc. (While it's not important for
plugins that are extending application-supplied menus, it *is* for
plugins that are extending each other's menus.)
This strongly suggests that some sort of strings are needed to identify menus.
Eclipse uses a simple concept of "menu paths", where each menu (or
toolbar) item or group has an identifier that names it relative to
its parent. This seems fairly workable, except that it doesn't
handle issue #2 above -- that is, plugins can't conditionally create
menus; they either have to create a new one or extend an existing one.
(During the time that I used Eclipse, I was bothered by the fact that
most plugins seemed to extend the menus in truly hideous ways,
usually by creating a submenu with all the plugin's commands. Now
that I've actually looked at how Eclipse implements menu insertions,
I can see why: extending a menu appears to require that you insert
into a group that's already been designated for extension. You
really can't insert something "between X and Y".)
Of course, to do conditional creation such as two plugins defining a
"Tools" menu, you'd have to include redundant information in each
plugin, so that the system can figure out whether the two "Tools"
menus are indeed the "same" menu, and also so that the case where the
menu is missing can be handled.
For constraint target references, it would be sufficient to ignore
paths that haven't been registered. That way, it doesn't hurt to
specify good constraints. The only paths that either have to exist,
or be conditionally registered, are parent paths.
Commands vs. Actions
--------------------
One nice feature of Eclipse's "UI contributions" system is the
distinction between commands and actions. A command is an abstract
concept that can be applied to many things, whereas an action is a
specific implementation of the idea. (Like "Cut text" vs. "Cut
method" as actions, and "Cut" as a command.) The idea is that
keystrokes are bound to commands, rather than specific actions, which
simplifies keystroke assignments. However, in the simplest case, an
action could perhaps be considered its own command. In Eclipse,
commands are also grouped into categories, which makes it easier for
the user to find commands and assign keystrokes to them.
Styling/Skinning and Tagging
----------------------------
We need some degree of GUI toolkit independence and/or
"styling/skinning". But this can be handled using AddOns, to e.g.
specify sizes, colors, images, etc. The addon classes would be
specific to the relevant GUI toolkit, so when creating widgets or
controls the specific toolkit can just query the add-ons to get that
kind of information.
On the other hand, it may be a lot simpler to just treat these things
as "tag" objects, ala peak.binding "metadata" objects or PyDicia tags.
That is, when defining a menu item, you could just include a list of
arbitrary objects, to be used appropriately by whatever tools need to
use them. For example, you might have a wxColorInfo object that
contained color info, and just throw it into the "tags" of an
item. The item wouldn't know what any of these tag objects "mean",
and they would not be add-ons themselves.
But when the item is created (or perhaps later, when it's used as a
template to create some real UI object), a generic function can be
called on each of the tags in order to "apply" it to the target
object. And the application might consist of either directly
altering properties of some widget/control, or else registering some
info with an add-on of some type.
Note that this sort of tagging is highly extensible, and would work
for Eclispe-style "schemes", "contexts", and "commands". Most
importantly, it would allow these features to be added *later*! That
is, we could start with a very simple system initially.
In fact, the tagging system could be extended to allow different ways
of specifying whether an action is visible and/or enabled in a given
context. For example, you could potentially include things like
security permissions.
Keystroke bindings, for that matter, could possibly be implemented as tags.
Should it be possible for one plugin to apply tags to another
plugin's items? My thought is "probably". For example, if a plugin
wanted to register a keyboard mapping scheme, it should certainly be
able to reference another plugin's commands in order to do so. This
would likely be through a service of some sort, or at least a
different object structure.
Given this, it looks less silly for Eclipse to require every single
thing to have some.dotted.name to uniquely identify it. However, we
can probably do the same thing via imports.
For this use case, most of the downside to imports doesn't really
apply; you want to target a specific object, not a possibly-shared
"tools" menu, for example. We can also use strings in place of
objects to refer to optional objects that might or might not be
present -- the API could simply ignore the tagging or even manage it
*lazily*, by ensuring that the information gets added if and only if
the target is actually imported!
Event Handling
--------------
The UI objects defined in a plugin module are not the "live" objects
used in the UI itself, since there may be multiple windows open on
different target objects, and each needs its own "live" object(s) to
work with. For example, a "menu" component that uses the menu
definitions as its operating spec.
Such a component would need to put the menu items in order, track
assigned IDs, and all that sort of thing. In addition, it would need
to know whether a given command, action, menu, group, etc. should be
visible, enabled, and/or checked at any given point in time.
My initial thought on this is that each of these things would be
determined by the "AND" of various conditions. For example, if
"scheme" or "context" tags are in use, then they may cause the
visibility condition to be AND-ed with a condition for whether the
applicable schema is active.
Ultimately, however, each of these things needs to be boiled down to
a single cell, managed by a controlling component that does the
necessary add/remove, enable/disable, etc. for each menu or toolbar item.
Similarly, the "checked"-ness of toggles and radio groups would
similarly need to be mapped to a cell, but this would likely be
handled directly by the action or command itself. That is, it should
have a way to take the interaction model component that's being
viewed (by the window containing the menu or toolbar) and return a
cell whose value is true or false.
For the side where the command/action is actually being applied,
there are a few possibilities.
First, the command could simply be modelled as firing a discrete
cell, and the corresponding actions could just check for their
applicability conditions. Or to put it another way, we could model
actions as add-on Components that get attached to the interaction
component, and know their enabled/disabled visible/invisible
checked/unchecked statuses, and refer to a "firing" cell provided by
the corresponding Command.
If the Command fires and the action is enabled, it can then be
run. Meanwhile, the command can summarize its visibility and
enabledness, in that it is visible as long as at least one of its
actions is visible, and enabled as long as one of its visible actions
is enabled.
Action Precedence
-----------------
If it's possible for more than one action for the same command to be
enabled at the same time, it's necessary to have some sort of
precedence between them. The best thing would probably be to use the
"most specific" action, although determining that may be
non-trivial. PEAK-Rules provides an impressive amount of machinery
for tackling this question, and parts of it could be leveraged for
that purpose, even before the "Python expression -> criteria objects"
part of PEAK-Rules is complete. The tree building machinery could
likely be used to create a tree of cells that ensures absolutely
minimal calculation of updates.
We still have the sticky problem of ambiguities, however. It would
be best if we could detect the ambiguities and identify the plugin(s)
that are involved.
It's also going to be "interesting" to design a mechanism that can
handle this, that can still be scaled down to something simple. :)
Implementation Structure
------------------------
The rough idea I have at this point is that plugins register
functions as extensions of an interaction component. The function
can then register actions and commands as add-ons of the component,
and those add-ons can then monitor the component's state as
appropriately. When a presentation component wraps the interaction
component, it can query the add-ons in order to generate menus and toolbars.
This is a bit better than the idea of using flat data structures, as
it allows you to refer to things that don't exist (yet) by using
add-on addresses, making it easy to merge definitions between plugins.
This doesn't work as well for keyboard bindings, however, since the
command *definitions* need to be referenceable, not just the command
*instance* associated with a specific interaction
component. Keyboard bindings need to be effectively registered with
a context instead, and the contexts need to be added-on to the
interaction component.
So, menu/toolbar/groups are really the only things that can be
implemented as straight-up extensions of an interaction
component. Contexts, schemes, commands, "activities", and command
categories all need to be registered globally -- otherwise there is
no way for an application-wide key binding service to work, nor an
application-wide activity-enablement system.
Open issue: should menu/toolbar items/groups be registered as
extensions of the interaction component, or the presentation? It
seems they are actually part of a particular presentation, whereas
the commands and actions are part of the interaction component (and
should be tested with that layer). The downside to separate
registration is of course that you have to register two plugin
functions. It might be okay to let the presentation stuff get
registered and hooked to the interaction object, though, as long as
it can be done in a neutral way to be picked up by the presentation.
Conclusion
----------
The conclusion (for today) is that I don't know enough yet to reach a
firm conclusion. :) The rough structure seems right, but there may
be many devils in the details. It may be that the best thing to do
next is some prototyping.
More information about the PEAK
mailing list