Pam Tutorial

Pam provides an interactive, interpreted interface to the Amulet graphical interface development environment. Using Pam, one can rapidly prototype applications by creating graphical objects and callback procedures in Python, and then using Amulet as a backend for displaying the graphical objects and calling the Python callback procedures. For simplicity, Python only supports a subset of Amulet, although we believe that it is the subset used the great majority of the time.

This tutorial provides a very quick introduction to Pam's capabilities. To run the tutorial, enter the Python interpreter, then type the following two commands:

import pam              # import the pam module

Creating Windows

To create a window and display it using Pam, type the following three commands:

win = pam.Window(width = 400, height = 400)
pam.screen.add_part(win)
pam.update()

The first command creates an instance of a Pam Window object and sets the window's width to 400 and height to 400. The window's left and top are inherited from the Window class. In general, properties whose values are not specified when an instance is created will be inherited from one of the instance's ancestor classes.

The second command adds the window to the Pam screen. In order to display a window, it must be added to the screen object.


Creating Graphical Objects

Pam provides the programmer with a number of primitive graphical objects, such as rectangles, lines, and text. To create a rectangle, one would create an instance of a Pam Rectangle object:

rect = pam.Rectangle(left = 10, top = 10, width = 100, height = 50)
rect1 = pam.Rectangle(width = 50, height = 50)

In order to display graphical objects, they must be attached to an appropriate window. Objects may be attached to a window using the add_part command:

win.add_part(rect)
win.add_part(rect1)

To view the objects, type:

pam.update()

Setting Graphical Properties

The graphical properties of an object may be altered by modifying the appropriate instance variables of the object. For example, to move rect1 to the coordinates (100, 100), one would type:

rect1.left = 100
rect1.top = 100
pam.update()

Viewing the Properties of an Object

The values of all the properties in an object are displayed using an object's print_contents method. For example, typing:

rect1.print_contents()

produces the output:

am_object            ==>  3
fill_style           ==>  <PredefinedStyle instance at c7b28>
height               ==>  50
left                 ==>  100
line_style           ==>  <PredefinedStyle instance at c7b10>
owner                ==>  instance of 
parts                ==>  []
top                  ==>  100
visible              ==>  1
width                ==>  50

am_object is an internal instance variable used by Pam. The owner and parts variables are discussed in the section on Groups. The visible attribute controls whether the object is displayed in the window to which the object is attached.

Creating New Graphical Object Classes

One can create new graphical object classes using Python's class syntax. For example, to create a red square whose width and height variables depend on a length variable, one could type:

class RedSquare (pam.Rectangle):
    fill_style = pam.Am_Red
    length = 10
    width = pam.Formula(lambda self: self.length)
    height = pam.Formula(lambda self: self.length)

Now one can create instances of red squares by instancing RedSquare. For example:

square = RedSquare(length = 40, left = 80, top = 100)
win.add_part(square)
pam.update()

Of course, to make RedSquare be truly a square, we would have to add code to prevent width or height from being set. Such code is beyond the scope of this tutorial.


Creating Behaviors

Like Amulet, Pam divides behavior into a small number of categories, such as move/grow, choice, and text interaction. Each of these behaviors is encapsulated in a class. To create a behavior, one creates an instance of the most appropriate class, and then customizes the behavior by modifying appropriate properties (behavior classes are called interactors and behavior objects are often called interactor objects). For example, to create a very simple behavior that moves objects around the screen, one could use Pam's move/grow interactor:

my_inter = pam.Move_Grow_Interactor()

Just like a graphical object must be attached to a window in order to display it, a behavior object must be attached to an object so that Pam knows which graphical objects to manipulate. In this case, we want the move/grow behavior to manipulate any of the objects in the window, so we type:

win.add_part(my_inter)

The Pam Event Loop

In order for Pam to process events, the programmer must call Pam's event loop:

pam.event_loop()

Pam's event loop continuously reads events, dispatches them to the appropriate interactor objects, and then updates the display after each event has been processed. For example, to move around the objects in the window, position the mouse cursor over the object you want to move, press down the left mouse button, and then, with the left mouse button depressed, drag the object around the display. When you have the object appropriately positioned, release the mouse button.

To get back to the Python interpreter, type Meta-Shift-F1 in the Pam window. (The Pam window is labeled Amulet).


Constraints

Pam allows formulas containing arbitrary Python code to be attached to the instance variables of a Pam object. These formulas may depend on other Pam instance variables and will be reevaluated automatically when any of these variables change value. For example, one could define the right side of rect to be equal to its left side plus its width using the formula:

rect.right = pam.Formula('self.left + self.width')

self is a reference to the object to which the formula belongs, in this case, rect. If one now types rect.right at the Python interpreter prompt, the current sum of rect.left and rect.width will be printed.

If the value of rect.left or rect.width is changed, the value of rect.right will be automatically updated. For example:

>>> rect.left = 40
>>> rect.width = 60
>>> rect.right
100
>>> 

A formula can also reference instance variables in other objects. This can be accomplished in one of two ways. The first way is to directly reference the other object. For example, to constrain the left side of rect1 to be 20 pixels to the right of the right side of rect, one could type:

rect1.left = pam.Formula(lambda self: rect.right + 20)

Note that instead of using a text string, you must use a lambda function in this case. In general, you must use a lambda function whenever you reference a variable or function outside the object to which the formula is attached (i.e., whenever you reference a variable or function that is not prefixed with self). The reason is that such references are considered references to global variables and there must be some way to capture the global dictionary for these variables. A lambda function captures this global dictionary.

A second way to reference instance variables in other objects is through pointer variables. Using this approach, you place a reference to the object you want the formula to refer to in an appropriate instance variable. The formula then references the instance variables in this object by going indirectly through the instance variable that points to the object. For example, if we want to constrain the top of rect1 to be equal to the top of rect, one could type:

rect1.top_obj = rect
rect1.top = pam.Formula('self.top_obj.top')

An advantage of pointer variables is that one can make a formula reference arbitrary objects without having to change the formula's function. For example, if one wanted to create a feedback object that appeared over the currently selected object, the programmer could create an instance variable in the feedback object called selected_obj that pointed to the currently selected object. The formulas in the feedback object could then reference the coordinates of the selected object via selected_obj. Using the direct reference approach (i.e., the first way), the feedback object's formulas would have to be changed each time a new object was selected. Using the second approach, only the selected_obj variable would have to be altered.


Groups

Pam provides a grouping class that allows a programmer to construct more complex objects from the primitive graphical objects that it provides. Pam also supports the automatic inheritance of the parts of a group when an instance of a grouping object is created.

For example, suppose we wanted to create a pyramid of two boxes, with one box centered above the other box. The following declaration would build the pyramid object:

class pyramid (pam.Group):
    components = [
         ('apex',
	   lambda: pam.Rectangle(
		left = pam.Formula('self.owner.width / 4'),
		top = 0,
		width = pam.Formula('self.owner.width / 2'),
		height = pam.Formula('self.owner.height / 2')
	      )),
	  ('base',
	     lambda: pam.Rectangle(
                left = 0,
		top = pam.Formula('self.owner.apex.height / 2'),
		width = pam.Formula('self.owner.width'),
		height = pam.Formula('self.owner.height / 2')
	     ))
	]

The parts of a Group object are declared using the components variable. The components variable should be a list that consists of name/function tuples. The name should be the name of the part and the function should be a parameterless function (typically a lambda function) that creates the part, as shown above.

When an instance of the group is created, the function for each entry in the components list will be executed, creating a part. A reference to the part is then placed in the variable defined by the name portion of the entry. In the above example, each instance of arrowline would create two parts, and place references to them in the variables apex and base.

Each part has a reference to its group placed in its owner variable. Hence each apex and base part would have an owner slot that pointed to the appropriate arrowline object.

The coordinates of a part are relative to the coordinates of its group object. Hence, to place the apex object at the top of the pyramid, its top variable was set to 0.

The following commands will create two different pyramid objects and display them:

p1 = pyramid(left = 100, top = 100, width = 40, height = 60)
p2 = pyramid(left = 100, top = 200, width = 100, height = 60)
win.add_part(p1)
win.add_part(p2)
pam.update()

The two pyramids may also be moved by the move grow interactor that has been defined. Simply type pam.event_loop() and then try dragging one of the pyramids. Remember, Meta-Shift-F1 gets you out of the event loop and back to the interpreter prompt.


Styles

Pam's style object allows you to modify the appearance of a graphical object. Pam provides both (1) a standard set of pre-defined style objects corresponding to common attributes (the color red; a line-thickness of two pixels), and (2) a custom style object that allows you to create your own colors and line-thicknesses.

Continuing with our pyramid example, let's create a style object to color the first pyramid's apex gray:

s1 = pam.Style(r = .5, g = .5, b = .5)
p1.apex.fill_style = s1

Next, we'll use the pre-defined style object Am_Red to color the first pyramid's base red:

p1.base.fill_style = pam.Am_Red

Now we'll change the thickness of lines in the second pyramid's apex, using another style object:

s2 = pam.Style(thickness = 5)
p2.apex.line_style = s2

Finally, we will use another pre-defined style object to change the thickness of the lines in this pyramid's base:

p2.base.line_style = pam.Am_Line_2

As usual, type

pam.update()

to see the effects of these changes.


Finishing Up

To shut down Pam and allow it to clean up, type:

pam.exit()

This command will destroy any existing windows.