CPIA -- Chandler Presentation and Interaction Architecture
CPIA is used to construct almost all of the application user interface in Chandler. I'd like to begin with a quick overview of what CPIA is, then describe the parts of CPIA that are currently implemented and give you some background that's necessary for understanding CPIA in the 0.4 release. I'll even include information about changes since 0.4 and describe our plans for the future. Keep in mind that CPIA is a work in progress, which will unfold as we proceed.
What is CPIA?
CPIA evolved from brainstorming and design meetings with
Mitch Kapor,
Andy Hertzfeld, and myself (
John Anderson) during the summer of 2003. Some of the main goals that motivated the design were desires to provide:
- An application construction environment which lets advanced users, without programming experience, customize or build completely new applications
- High-level user interface widgets tailored especially to the needs of Chandler, e.g. powerful tables and calendar views
- A user interface that is abstracted from the particular platform it runs on, e.g. PCs, Web browsers, PDAs. (Note: throughout this document I'll only use "platform" to distinguish between desktop, Web, and PDA. I'll never use "platform" to distinguish between personal computer platforms, e.g. Linux and Macintosh and Windows).
CPIA has several new concepts that make it seem a little unusual. Perhaps the best way to understand CPIA is to compare it with existing UI toolkits. Most UI toolkits consist of objects, often written in an object-oriented programming language, that handle the display of a variety of widgets. Examples include tree controls, text boxes, and list views.
To use these objects, you have to be a programmer and write code that
- adds behavior
- sends data to the widget
- receives data from the widget
- saves the data in some sort of persistent storage, e.g. a file or database.
Even though the UI toolkit displays the widget for you, there's still quite a bit of programming necessary to make it work properly.
CPIA, in contrast, provides more complete user interface objects, which we call blocks. Blocks are designed to fit together like Lego blocks, requiring little or no programming. Blocks have several advanced properties:
- Blocks contain all the necessary code to load, display, and store data.
- Blocks automatically remember their state, for example a scroll position, selection, or window size.
- Rather than writing code to provide behavior, you typically set properties of a CPIA block. For example, to specify the data to display in a list, you set the contents attribute of the block to a query which in turn pulls the data to display from the Chandler repository. This is much simpler than writing code to load data from a file or database.
These properties set CPIA apart from traditional UI toolkits and represent the next logical step in the evolution of user interface development systems.
Blocks in CPIA communicate with one another using CPIA events (sometimes called block events in the code) -- high-level events that describe changes that happen in one block that affect other blocks. CPIA events are typically more high-level than the events in many UI toolkits. For example, CPIA might emit an event that indicates that a user selected an element in a list, but CPIA would not give low-level mouse-moved and key-press events.
With the exception of events sent by menus, the
SelectItem (formerly named
SelectionChanged in the 0.4 release) event provides almost all the behavior you see on the screen. We expect the number and type of CPIA events to evolve in future releases as we gain more experience.
Finally, like all other data in Chandler, CPIA blocks are stored in the Chandler repository. This lets us take advantage of all of the features of the repository, which include:
- The ability to access data as Python objects, making programming easier
- Transparent persistence which automatically loads data as the data is accessed and automatically saves the modified object
- A rich set of data types that map conveniently onto Python data types
- A uniform mechanism for describing the schema of all data as data
- A transaction system that allows for undo/redo
- A powerful query mechanism
All these features of the repository are used by CPIA today, with the exception of undo/redo and query, which are still in development.
For another description of CPIA, you might want to look at
a CPIA description which is much older, but provides information about some of the features of CPIA that are not yet in Chandler as of the 0.4 Release.
Inside CPIA
To delve deeper into understanding how a block works, it's often best to first look at its definition in the schema rather than first looking at the code, since much of the behavior of blocks is determined by data rather than code. This "data driven" approach is one of the key design choices of CPIA, which lets the behavior of the application be modified by changing attributes of a block rather than writing new code. Most of the block schema is defined in the blocks parcel located in
parcels/osaf/frameworks/blocks/parcel.xml. A basic understanding of the
repository data types and parcel XML is a prerequisite for understanding the block parcel; you might profit from reading
The Busy Developer's Guide to the Repository and
Projects.Parcel Manager. For those so inclined, you might read the following description of block attributes alongside the
block parcel XML.
Blocks are organized into trees, collectively called Views, with each block a variety of attributes, examples of which include:
- a parent and zero or more children. For example, a BoxContainer block lays out blocks in a rectangular area of the screen and contains children blocks which are laid out in the box.
- a block name,
- an ItemCollection,
- a style,
- a boolean indicating whether the block is visible,
- CPIA events that a View subscribes to.
Some of these are optional. To see which are optional, what the default values are or the other attributes of a block, check out the
block parcel XML.
Block methods
Blocks, like all items in the repository, appear as objects in Python, they contain methods as well as data. For example, all blocks have a method
post, which posts CPIA Events. You can see other methods by looking in
parcels/osaf/frameworks/blocks/Block.py.
(Note that while understanding some of these methods will give you a deeper understanding of how CPIA works, in practice, you will almost never need to call a block's methods directly. Almost all of the behavior that you care about is determined by a block's attributes.)
Another important method on Block, is
render: It calls
instantiateWidget (another method on the block) to create a nonpersistent, platform-dependent counterpart to the block. On the PC platform, the counterpart is a wxWidget object.
In some ways, perhaps you'd wish that the block and their platform-specific counterpart could be the same object. This turns out to be difficult since blocks can only contain data that can be persisted (saved and reloaded from disk the next time you run Chandler) from one run of Chandler to another. WxWidgets contain C language pointers, which when persisted don't make sense). This is not all bad: following the Model/View/Controller paradigm, it makes sense to separate the model, i.e. block, from the view, i.e. wxWindows widget. Separating the model from the view means that it will be easier at some future date to support browsers and PDAs as well as personal computers.
Rendered blocks have a "widget" attribute that lets you get its widget, likewise widgets have a "blockItem" attribute to get the block. An unfortunate limitation of the repository requires that all blocks be memory resident whenever they have a non-repository attribute like widget.
CPIA execution sequence
Another way to get a more in-depth understanding of CPIA is to follow the sequence of events that happens when Chandler starts up and displays the user interface:
- Code begins executing Chandler.py, which does little more than create and run an wxWidgets application object. The OnInit method of the application object in Application.py is where almost all of application initialization happens.
The entire user interface of Chandler begins with a single block in the repository, currently at a fixed repository path: parcels/osaf/views/main/MainView
The MainView block has methods that implement most of the applications menus, e.g. cut, copy, paste and much of the specialized functionality, e.g. sharing collections. The MainView is the root of the tree of blocks that represent all of the Chandler user interface. The children blocks of MainView include a BoxContainer that contains the main window's user interface, the menubar, which in turn contains all the menus, the status bar at the bottom of the main window, and several CPIA events that are automatically subscribed when the block is rendered.
- The MainView is rendered, creating a wxWidget widget corresponding to the block. After a block is rendered all of its children are rendered. After all of its children are rendered, the synchronizeWidget method on the block is called, whose job it is to make sure that all of the attributes of the block are properly reflected in the widget, for example it will hide the widget if the blocks isShown attribute is False. For all blocks that are rendered it's possible to modify an attribute on the block and call synchronizeWidget to make the widget reflect the attributes of the block.
- Now that all the block's widgets are synchronized to the block's attributes, the user interface is visible on the screen and the application is ready to process of events.
Menus
The way menus work also provide insight into the design of CPIA.
Menus can contain
MenuItems and other
Menus and
Toolbars can contain
ToolbarItems. You can make a
Menu,
MenuItem,
Toolbar or
ToolbarItem the child of any block in the tree of blocks that make up a View. When the active view changes, by clicking on an item in the sidebar, all the
Menus,
MenuItems,
Toolbars and
ToolbarItems above the are combined to make up the actual menus and toolbars displayed in the user interface. Thus, by simply adding a menu block as a child of your View, you can specify that your new
MenuItem be appended, inserted before another item, replace an existing item, or delete a
MenuItem in any
Menu of your application. This simple mechanism provides the ability for a view to completely customize the menus and toolbars, since the mechanism works exactly the same way for
ToolbarItems in
Toobars, without have to write any code. Each
ToolbarItem and
MenuItem contains a reference to a CPIA event. When the menu is chosen or the Toolbar button is pressed the event is dispatched.
Events
The
dispatchEvent method of View is responsible for dispatching CPIA events to particular blocks. There are several ways CPIA events can be dispatched, determined by the
dispatchEnum attribute of the CPIA event:
- They can be sent to a particular block.
- They can be sent to the block that has the keyboard focus or the block that is the active view, and bubbled up to the first parent block containing it that can handle the event.
- They can be broadcast to all blocks that are currently rendered on the screen, or all the blocks within a boundary. The set of blocks within the boundary is determined by recursively following children and parents of a given block until a block with an attribute eventBoundary = True is encountered. This makes it possible to add special functionality to your view and provide menus and toolbar items that appear only when your view is active. It also generalizes to allows the nesting of views.
We implemented a mechanism similar to wxWidget's
wxUpdateUIEvent. It sends an special event just before a user interface widget is displayed that gives the application a chance to specify the state of the widget: checked, enabled, or the string to display. This simplifies the application design because it doesn't need to keep all the user interface in its correct state, it only needs to set the state of a widget when it's necessary. To use this mechanism, just implement a method with the same name as the event's method appended with "
UpdateUI". For example, if the
Undo CPIA Event has a methodName attribute "
OnUndoEvent", it will call a method named
OnUndoEventUpdateUI just before the menu is displayed. You can return in the notification's data dictionary a boolean for the key "
Enable" to enable the menu; a boolean for the key "
Check" to check the menu, and a string for the key "
String" that will be used to display the text of the menu. The same mechanism applies to
MenuItems.
CPIA events are notifications, and consequently are subject to the limitations of notifications. Currently, notifications must be subscribed to before they can fire, and these subscriptions are not persistent. Each block has two optional attributes,
subscribeWhenVisibleEvents and
subscribeAlwaysEvents which are bidirectional references between the block and events. Whenever a block is rendered,
subscribeAlwaysEvents are automatically subscribed. Whenever a block is rendered and visible the
subscribeWhenVisibleEvents are subscribed.
Forgetting to add a new event to the list of subscribed events is a common source of errors. Finally there is a list of Global events which are always subscribed and are application independent. They are defined in
parcels/osaf/frameworks/blocks/events/parcel.xml.
You should be careful to not add any Chandler specific events to this global list. In the 0.4 release some events don't follow these guidelines, but the latest code in CVS does.
Besides being attached to a menu or toolbar, block events can be posted by code. Shortly after 0.4 we added an
eventName attribute to events and a method on block,
postEventByName that posts an event by name. We keep a dictionary mapping
eventName to event as we subscribe events when blocks are rendered and unsubscribe them when widgets are destroyed. If two events have the same name, the first one subscribed will be posted.
More About the Kinds of Blocks
Next I'd like to give a short summary of each of the different kinds of blocks, with an eye towards those blocks whose design we're happy with, and those whose design we have decided to change based on our 0.4 experience.
Blocks whose design we're mostly happy with:
- BoxContainer: A rectangular box that contains child blocks, e.g. Tables or other containers, and lays them out horizontally or vertically. BoxContainers are implemented in Chandler with wxWindows sizers and have similar layout properties.
- TabbedContainer: A tabbed panel where each pane is a child block as in BoxContainers.
- List: A two-dimensional table containing rows and columns, where each cell can only be text. The contents of the list are determined by a delegate, described below. Besides the delegate, other notable attributes of List include: the strings displayed in columnHeadings, widths of columns, a data value used by the delegate, and the selection.
- Table: A two-dimensional table containing rows and columns, where each cell can contain almost any kind of data. Tables are more heavyweight than Lists, but share the ability to control what is displayed by a delegate. In addition tables use attribute editors, described in more detail below, a powerful new mechanism, partly implemented in 0.4, which is designed to unify the display and manipulation of different types of data throughout CPIA. Attributes of Table are similar to List.
- Tree: A tree list, where each node in the tree is similar to a row in a List, and like List uses a delegate to determine what is displayed. Attributes of Tree are similar to List.
- Menu, MenuItem: Menus and the items they contain. MenuItems contain both information about how to display the menu and what CPIA event is sent when the menu is chosen.
- Toolbar, ToolbarItem: A toolbar that lays out its children Blocks in one dimension.
- DynamicContainer, DynamicChild are not standalone blocks, they are mixed in to other block type, e.g. Menu, MenuItems, Toolbar, ToolBarItems blocks to allow many items in different locations to appear like children of a Menu or Toolbar.
- SplitWindow: A splitter window that has two children, one for each split
Blocks whose design we're less than happy with, and hope to have refactored for 0.5:
- MenuBar: The topmost Menu. We are looking into eliminating the distinction between MenuBar and Menu
- StatusBar: The bar that is typically shown at the bottom of the Window. The StatusBar needs to be rewritten to make it be more like a BoxContainer, containing a list of children which are the attribute blocks.
- ContainerChild: This block was eliminated shortly after the 0.4 release
- ItemDetail, SelectionContainer, ContentItemDetail: These blocks will be rewritten to take advantage of our new design to view an Item with a different tree of blocks for each kind of Item.
- Button, Choice, ComboBox, EditText, HTML, RadioBox, StaticText, will all go away and be replaced with the attribute block that displays and edits attributes on items (see discussion of Attribute Editors below).
- LayoutChooser will become a TabContainer with a different style attribute.
- ScrolledContainer will become a BoxContainer with a different style attribute.
- ContextMenu will become a Menu.
Trees,
Tables and
Lists all use a delegate to determine what data is viewed in the cells. The delegate implements methods that return the number of elements or columns in the table, the type of data in a row and column, the actual data in a cell and the text in the heading. By writing your own delegate you can completely control what a
Table,
List or
Tree displays. Although this may seem like a departure from the data driven design of CPIA, the delegate is refered to by string attribute of the block and there are two main preexisting delegates:
ListDelegate and
AttributeDelegate (
SummaryTableDelegate was absorbed by
AttributeDelegate shortly after the 0.4 release).
AttributeDelegate can be used without subclassing to display
Tables where a row consists of the items from an
ItemCollection (described below) and columns represent a particular attribute of the item. To use a
ListDelegate you need to subclass it and implement
GetElementValue. The repository viewer, (
/parcels/osaf/views/repositoryviewer/Repository.py) contains a short example of a custom delegate.
ItemCollections
ItemCollections, although not strictly part of CPIA, are important to understand in the context of CPIA. An
ItemCollection contains a rule, which specifies a set of items in the repository. An example might be "all the items that are of Note kind" (for more on rules, check out
Query System Reference). An
ItemCollection can also be viewed as an array of items, called the results, which is just the a recent result of the running the rule. From CPIA's point of view, a
Table or
List often displays the list of Items in the
ItemCollection, so the array is the main interface to the
ItemCollection. In addition to the rule, an
ItemCollection also contains two other lists:
Inclusions and
Exclusions -- explicit lists of items to add or remove after the rule is run -- and a list of Kinds that filter the results after inclusions and exclusions have been added so the results contain only certain kinds of items.
Perhaps the most interesting feature of
ItemCollections is that the repository can know when items are created, deleted or modified, and since it can know the rule, it can usually tell when the results change, making it possible to incrementally update the results and even notify the block that it needs to update the user interface when the results change.
ItemCollections have methods to subscribe and unsubscribe to notifications about changes, add or remove items from the inclusions and exclusions, add or remove the filter kind, get and set the rule, access the results by indexing, getting its length, etc. In the future we will be adding sorts to
ItemCollections, taking advantage of the repository's recent ability to index ref collections. See
parcels/osaf/contentmodel/ItemCollection.py and
repository/query/Query.py. for more details.
Upcoming Changes
Besides the changes to blocks that we're not completely happy with, there are several important upcoming changes to CPIA. While there are several upcoming changes, the two biggest are attribute editors and a redesigned detail view.
Attribute Editor
The attribute editor is responsible for displaying and optionally editing the attribute in
- a table cell,
- an attribute block contained in a tree of blocks (e.g. a field in the detail view), or
- a field in a dialog.
In the 0.4 release, we used attribute editors in
Tables, but not in the detail view or dialogs. Attribute blocks have a
contents attribute that contains an reference to an item and an attribute name, along with a style attribute that is used to give a hint about how to display the attribute. Just as the contents automatically notifies the block of changes to
ItemCollections, an attribute block will be notified when its attribute changes.
Button,
Choice,
ComboBox,
EditText,
HTML,
RadioBox,
StaticText will all be replaced with the attribute block, where the type of data and style will determine which control is displayed. Attribute editors will also implement a mechanism for data validation. We expect that it will be possible to add new attribute editors or styles to display existing attribute kinds in new ways or new kinds of attributes in new ways.
Detail View
Today there is one detail view, a tree of blocks that is modified by code that detects changes to the kind of item displayed. In the future, we will have a separate tree of blocks for each kind of Item which isn't modified by special purpose code. This will allow the addition of a new tree of blocks to display a new kind of Item without the addition of code. We hope this will make it easy for external developers to easily extend Chandler, or build completely differeent application on top of CPIA. This will also eliminate the need to explicitly create many slightly different trees of blocks in parcel XML.
Repository Paths
Today most blocks are addressed with repository paths. In the future, we expect that only the repository will internally use repository paths. We will have a new addressing scheme for use outside of the core repository code. We also intend to divide the repository into three conceptual parts:
- the core portion that no repository can live without, where items are populated with repository packs instead of parcels,
- a read-only portion populated with parcels, and
- a read-write portion where content items and other user visible items (e.g. blocks) are stored.
Parcels would only load items into the read-only portion. The parcel loader and CPIA would allow us to copy trees of blocks into the read-write portion where user modified versions would live.
Finally, we hope to revisit the location of parcels in the file system, and the corresponding read-only location.
Build Mode
We envision a future where there is a "build mode" switch that can be flipped on in any CPIA application. While in "build mode", you will be able to inspect and edit the attributes of a block without having to edit raw XML. Eventually we would add controls to the user interface in build mode so blocks could be resized, deleted or new blocks inserted. In this way we hope to make it possible to customize or even build new applications with little or no programming.
Internationalization
In the past we've had a strategy for internationalization, which we haven't rigorously followed, nor have we taken the time to build different versions of Chandler for different locales. We need to revisit this issue to make sure that our design is adequate to support localization.
Commiting Repository Changes
New data created by the user gets written to the repository when we call repository.commit(). The Chrome calls commit any time a user creates a significant piece of new data, e.g. they create a new item or a new collection. To understand how commit works, we need to understand repository views (not to be confused with CPIA views). The repository supports multiple views, which you can think of as separate sets of data changes. Reading email through Twisted is done in a separate view. Commit is a three step operation: 1) changes from other repository views are reconciled with the current view. 2) Notifications are sent about data that has been changed. 3) The current view is written to the data base. A new repository API was added in 0.4 called refresh() which does the first two operations, but skips the time-consuming database update.
In many cases in CPIA and the Chrome we were calling commit solely to recocile data changes and trigger notifications. We're now in the process of reworking CPIA and the Chrome to use refresh instead of commit when we don't need the data written right away. In the longer term, we plan to use the roll-back capability of the repository with commit to implement Undo. We'll commit changes in views that correspond with user documents. Then when an Undo operation is needed, we can roll back the commit in that view to get back to the previous data state.
Special thanks to Ducky Sherwood for editing, formatting and feedback.