[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