[PEAK] A layout and styling framework for UI components
Phillip J. Eby
pje at telecommunity.com
Thu Dec 6 16:30:17 EST 2007
This is a rough draft of how I'll be implementing a new package
called "Presentable", containing peak.ui.rendering, peak.ui.layout,
and other modules.
The purpose of this package is to provide a basis for implementing
and testing platform and GUI framework-independent presentation
layouts, which are then transformed to specific widget sets, styles,
colors, and spacing, using style sheets or "skins".
One of the main motivations for doing this is that creating wx widget
layouts by hand becomes incredibly painful as the layouts get more
complex. There is a complex order in which the widgets must be
created and initialized, plus a bewildering variety of options and
special APIs needed to do adequate positioning and alignment.
Requirements
------------
One of the biggest complications involved is that GUI components must
typically be initialized in a top-down fashion, beginning with a
parent frame followed by its child controls. However, in writing
programs, it is much easier and clearer to specify objects in a
bottom-up form, e.g.:
Frame(children=[Control1(), Control2(), ...])
(Notice that an expression like this actually creates the parent
*after* its children.)
So, one of the design goals for the layout framework is that you
should be able to specify a presentation-component layout of
arbitrary complexity using only a single expression, without needing
to break it down into statements like this:
frame = Frame()
ctrl1 = Control1(frame)
ctrl2 = Control2(frame)
...
frame.children = [ctrl1, ctrl2, ...]
Of course, this means that the objects used to specify the layout
cannot be the same objects that are used to *implement* the layout,
since we can't make wx (or most other UI frameworks) play nicely with
this approach.
But that's okay: for many things, that's what we want anyway. It
should be possible to take, for example, interaction model feature or
scope objects, and treat them as inputs to the layout system. For
example, one should be able to lay out a bunch of interaction-model
Field objects, or treat Scopes as panels in a frame.
In other words, the layout framework should allow you to use it as
glue that wraps just about any sort of application object, and turn
it into widgets of one kind or another, by applying style/skin information.
Further, there should be a way to easily specify horizontal or
vertical concatenation of target objects, and this specification
should *not* require any explicit spacing or positioning
information. Instead, it should be possible to have "layout hints"
of some kind to determine the appropriate inter-component spacings.
In addition, it should eventually be possible to do mark-based row
and column layouts (similar to the operations of Basser Lout), to
automatically produce "grid bag" style constructs.
The framework should also include some standard classes for things
like splitters, panels, frames, etc., and include a module that maps
them to default wx implementations. But none of these classes should
depend on wx or the wx support module; instead, the default rules for
a GUI framework should only be activated if the framework in question
is imported.
The mapping from a set of abstract layout and interaction components
to specific widgets and options should be done through a "skin" or
"style sheet", and these skins or style sheets should be able to
"inherit" settings from simpler skins or style sheets, all the way
back to a default skin or sheet for the target GUI framework. Skins
or sheets should also be able to include conditional values based on
the target platform, since wx often requires different flags or
settings when running on different platforms. They should also be
able to handle the translation of abstract hints like "this is an
alert" into what visual or behavioral properties an "alert" widget should have.
Finally, it should be noted that the primary focus of the framework
will be for creating semi-"static" layouts - that is, ones where all
the widgets to be created are known at creation time.
This does NOT mean that you won't be able to create highly dynamic
layouts, including programmatically generated ones. It's just that
some features may only work well if the layout is known at rendering
time. You can, of course, still render sub-layouts and add them to
an existing widget set if you know what you're doing.
To put it another way, widget rendering is generally not going to be
a trellis-controlled activity, and widgets aren't necessarily going
to be created or destroyed automatically when the data set from which
they were built changes. (The widgets themselves, of course, will
respond to changes in the data models they're attached to.)
Tree Transformation
-------------------
So, we are basically looking at doing a type-agnostic tree
transformation or "visitor" pattern, which is something that generic
functions excel at.
If possible, I'd prefer to see if there's a way to use simplegeneric
for this, without having to draw on the full power of a RuleDispatch
or PEAK-Rules. (This is more of a practical concern than a
theoretical one -- I need the layout and interaction frameworks done
*yesterday*, and PEAK-Rules is a few days work short of the necessary
features.)
There are a few basic operations that the tree transformer needs to
be able to do:
1. Determine what constructor and initial arguments to use to create the widget
2. Do any pre-child widget setup (e.g. setting wx properties and
options that aren't specifiable via constructor)
3. Do any per-child operations (e.g. adding to sizers, spacers, etc.)
4. Do any post-child setup (e.g. calling Realize(), Layout(), Show(), etc.)
In principle, one could write this as four generic functions, each
keyed off of the platform, GUI framework, skin, and item to be
transformed. But there are some complications, apart from the fact
that we would prefer to avoid multiple dispatch for now.
Mainly, the complication is the fact that we want to allow multiple
"hints" per layout item, and trying to make a generic function that
dispatches on such arbitrary items is difficult.
So, instead of trying to combine methods, generic function-style, we
need a way to combine the *results* of running various methods. In
other words, we need...
"Renderer" Objects
------------------
A renderer is a relatively simple object that exists to hold the
state for, and co-ordinate the process of, rendering the presentation
for an object. It performs widget construction and
pre/per/post-child processing, after first being "configured" for the
object(s) being rendered. It also holds references to the target
object, the resulting rendering, and a parent renderer if
any. Renderers will also be able to "manage" context managers, such
that the rendering process can be wrapped by context managers (such
as the application of nested style sheets).
A renderer will also be the placeholder to which various AddOns can
be added, each containing configuration data for the kind of
rendering being produced. These AddOns will usually be GUI
framework-specific, in that addons for wx widgets will be distinct
from those for Qt, etc.
Last, but not least, a renderer will possess "handler lists" for the
four main operations of pre/post construction, and per/post child
addition -- and an extra handler list for the process of *locating*
the children to be added. These handler lists will all have their
contents called at the appropriate times.
Thus, the renderer type should be fully generic, as instances will be
configured dynamically for the object being rendered.
But how? Well, when a renderer is created, it will look up its
target's type in a "skin" to find zero or more functions to call that
will configure the renderer. These functions will be able to add
handlers or directly configure the renderer in any way desired.
So let's look at...
Skins and Style Sheets
----------------------
A "style sheet" is something that contains specifications for how
arbitrary objects are converted into actual presentation (such as wx
widgets and trellis-based event bridges).
Style sheets will be defined using class syntax, allowing the
possibility of inheritance, e.g.:
class wxStyles(DefaultStyles):
"""General styles for wx-based objects"""
class MyAppStyles(DefaultStyles):
"""My application's generic style additions"""
class MyAppInWX(MyAppStyles, wxStyles):
"""My application + wx"""
A "skin" is actually an *instance* of a stylesheet, and can contain
mutable state information (although this isn't necessarily a good idea).
Skins can be dynamically "subskinned", to obtain a new skin that
mixes the original skin's stylesheet with zero or more additional
stylesheets. For example, if you call ``.subskin(BarStyles)`` on an
instance of ``FooStyles``, the returned skin will effectively be an
instance of:
class FooBarStyles(BarStyles, FooStyles):
"""Automatically-generated, merged stylesheet"""
and any instance attributes of the original skin will be copied into
the new instance.
You will also be able to "update" existing skins with additional
content, using the .update attribute, e.g.:
class more_wx_styles(wxStyles.update):
"""Anything here will get added to wxStyles"""
The idea here is that when you define components that need default
style information, you can just mix it in to an existing
stylesheet. (This prevents the need to mix a zillion stylesheets, or
to have to have a different API for adding new rules to existing stylesheets.)
One caveat: you won't be allowed to define any non-rule attributes or
methods in an ``.update``, as this could lead to conflicting or
confusing results. Attributes and named methods can only be
introduced through normal inheritance in a new stylesheet. (Oh, and
you can only reference one ``.update`` as a base class - no multiple updates.)
So what are these "rule" methods?
Rules and Cascading
-------------------
A rule is just a method of a skin (stylesheet instance) that takes a
renderer and target object as additional parameters. The method is
free to do whatever it wants to the renderer, such as add handlers,
set up the constructor, or annotate it with configuration data and add-ons.
Rules will be registered in the body of stylesheet definitions,
usually something like this:
from peak.ui import rendering
class MyAppStyles(rendering.DefaultStyles):
"""My application's generic style additions"""
@rendering.rule(SomeApplicationType)
def render_sometype(self, renderer, subject):
# code to configure the renderer
All rules that apply to a given type are executed, in order of the
target instance's MRO, then in order of the stylesheet's MRO. So,
for example, if you have this hierarchy:
class Foo(object): pass
class Bar(Foo): pass
class MyStyles(rendering.DefaultStyles): pass
Then the order of rule precedence for a Bar instance is:
* Rules defined in DefaultStyles[object]
* Rules defined in MyStyles[object]
* Rules defined in DefaultStyles[Foo]
* Rules defined in MyStyles[Foo]
* Rules defined in DefaultStyles[Bar]
* Rules defined in MyStyles[Bar]
Note that more-specific rules are executed *later*, which thus gives
them the opportunity to override things that are set up by a
less-specific rule. (Note: a given stylesheet may contain only one
rule for a given target type, and once defined, it cannot be
changed. Among other things, this allows for caching.)
Also note that in the common case, rules are going to do nothing but
add *other* functions to the renderer's handler lists. So, we may
need some other objects or decorators or convenience functions, to
make this common task less repetitive. However, what exactly that
will look like is left unspecified for the moment, until we have a
better idea of what's needed.
What's Left
-----------
This post doesn't actually cover how layouts will work, although it
can be taken as a given that most layout requirements can be met
through simple hierarchical construction and specifying of options on
renderers. I will probably post again at a later time regarding more
sophisticated layout possibilities, but it will likely be after work
on how to "hint" or "tag" interaction components (such as commands,
fields, etc.) for use in rendering menus, toolbars, dialogs, and such.
Similarly, this post doesn't go into what sort of abstract layout
components might be available by default, or what the standard way of
"wiring" various interaction components would likely be. There's a
fair amount still to be accomplished in these areas, but I suspect
that design work in these areas will be more productive if left until
after the basic rendering framework is available.
More information about the PEAK
mailing list