Pam--A Python Interface to Amulet

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.

Design

Like Amulet, Pam is designed to offer a number of features supporting the construction of interactive, graphical applications, including:

Constraints

Pam provides a Formula class that can be used to create arbitrary formulas.

Instance Variables

There are no public instance variables in a formula. Once a formula is created, it should be attached to an instance variable in a FormulaObject (see next section). The value computed by a formula can be accessed through the instance variable to which it is attached.

Methods

Formula(expression)

Formula takes an expression and converts it into a formula object. expression may be either a string (e.g., 'self.x + self.y') or a function (e.g., lambda self: self.x + self.y). If a formula wants to reference an instance variable in the object to which it will be attached, it should preface the name of the instance variable with self. (e.g., self.x).

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.

print_contents()

print_contents prints the values of the fields in the formula data structure. This function can be useful for debugging a formula.

Pointer Variables

Pam allows formulas to indirectly reference instance variables in other objects via indirect references. For example, suppose that a feedback object for a move/grow operation should assume the dimensions of the object being moved or resized. This objective can be achieved by 1) storing a reference to the object being moved or resized in an instance variable in the feedback object and 2) writing the feedback object's width and height constraints so that they access the coordinates of the moved/resized object indirectly through the "pointer" variable. For example, if the pointer variable's name is obj_being_changed, then the width and height constraints could be written as:

feedback.width = pam.Formula('self.obj_being_changed.width')
feedback.height = pam.Formula('self.obj_being_changed.height')

To handle cases where the pointer variable should be null, Pam defines a special global constant called FNULL that represents a null object. If a formula tries to make an access through a variable with the FNULL value, the formula will simply return its old value and terminate. For example, when no object is being moved or resized, the obj_being_changed variable could be set to FNULL. When the application accesses the feedback object's width or height variable, it will get the last computed value for these variables.


Basic Object System

Pam defines a FormulaObject class that provides three types of functionality:

  1. it inherits default values, including formulas, from ancestor classes. Values from more immediate ancestors override values from more distant ancestors,

  2. it allows formulas to be attached to instance variables, and

  3. it allows the user to specify the values of instance variables when an instance is created.

Default Values

Default values are defined in the same fashion as ordinary Python instance variables. For example:

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

These values will be inherited by any instance objects that are created, unless they are overridden by a subclass, or are overridden by the user. Unlike an ordinary Python class variable, you cannot change a Pam class variable without possibly causing strange behavior. In particular, if you change a Pam class variable it is possible that formulas that depend on this variable will not be updated, and that instance objects will not appear to inherit the new value immediately. It is permissable to override a class variable at the instance object level. For example, while changing left in GraphicalObject could cause unexpected behavior, changing left in an instance of GraphicalObject would cause expected behavior.

Attaching Formulas to Instance Variables

Formulas may be attached to a variable via assignment:

obj.name = pam.Formula(...)

For example, to assign a formula to a.bottom, one can write:

a.bottom = pam.Formula('self.top + self.height')

Removing Formulas from Instance Variables

A formula may be removed from an instance variable via remove_formula:
obj.remove_formula(name)

name should be the name of an instance variable. For example, to remove a formula from rect1.left, one would type:

rect1.remove_formula('left')

Instance Creation

When creating an instance of a FormulaObject or one of its subclasses, the programmer may include values or formulas for instance variables. These values may override the default values or they may add new instance variables. The general format for instance creation is:

name = ClassName(variable1 = value, variable2 = value, ...)

where the variable names should be strings and the values may be either simple values or formulas. For example:

a = pam.Rectangle(left = 30, 
                  right = pam.Formula('self.left + (2 * self.width)'), 
                  value = 20)

Methods

print_contents()

print_contents prints all of a FormulaObject's variables and their values. Formula values are printed as Formula(value, valid flag), where value is the value last computed by the formula and the valid flag is t if the value is up-to-date and f if the value is out-of-date. For example, immediately after a is created in the above example, a.print_contents() would produce the output:

am_object            ==>  2
fill_style           ==>  <PredefinedStyle instance at c7b28>
height               ==>  10
left                 ==>  30
line_style           ==>  <PredefinedStyle instance at c7b10>
parts                ==>  []
right                ==>  Formula(0,'f')
top                  ==>  0
value                ==>  20
visible              ==>  1
width                ==>  10

The slots shown in addition to left, right and value are slots that are inherited from the instance's superclass (pam.Rectangle).


Graphical Objects

Pam provides a number of graphical objects that can be instanced or subclassed, such as windows, rectangles, lines, text, etc. Pam uses a retained object model, which means that each object displayed on the screen has a corresponding object in memory. Graphical objects have properties, such as position, size, and color, that may be altered by modifying the appropriate instance variables.

Instance Variables Common to All Graphical Objects

All graphical objects have the following instance variables and default values:

variable 	    default	description
 name		     value

left			0       object's left side
top 			0       object's top
width		       10	object's width
height 		       10	object's height
visible 	     true	whether the object is visible
line_style       Am_Black       style of object's line segments 
fill_style       Am_White       style of object's interior
as_line             false       whether the object should be treated as
                                      a line or non-line
as_circle           false       whether the object should be treated as
                                      a circle or non-circle
The line_style and fill_style attributes may be set to pre-defined values representing useful attributes (e.g., the color red), or to custom values created using the Style object. See the Style Objects section below for details. The as_line and as_circle attributes control how interactors, such as the move-grow interactor, interact with the object (e.g., if an object is treated as a circle, the move-grow interactor will set the object's diameter slot rather than the object's width and height slots.

Methods Common to All Graphical Objects

destroy()

This method destroys the graphical object and removes the object's image from the screen. The destroy command is also called when the object is garbage collected.

point_in_object(x, y, ref_obj)

This method returns true if the point lies within the object and false otherwise. ref_obj defines the coordinate system for the point. Usually the reference object will be a window, but sometimes it could be a group object.

to_top()

This method moves the object to the top of the stacking order in its group (or window if the object's owner is a window)

to_bottom()

This method moves the object to the bottom of the stacking order in its group (or window if the object's owner is a window).

move_object(ref_obj, above = true)

This method moves the object to just above the reference object in the stacking order if above is true, and otherwise to just below the reference object. above is an optional parameter that defaults to true.

Graphical Objects

Rectangle

A rectangle object may be created by instancing Rectangle. For example:

my_rect = pam.Rectangle(left = 20, top = 50)

Polygon

Pam provides both absolute coordinate polygons and scalable polygons. An absolute coordinate polygon is specified via absolute coordinates for its point list and may not be scaled, although it may be moved. A scalable polygon is specified via fractional values between 0 and 1 for its point list and by providing a left, top, width and height. The polygon's coordinates are then computed by converting each fractional point to an absolute coordinate using the equations:

    x = left + (fractional_point.x * width)
    y = top + (fractional_point.y * height)

The relative attribute of a polygon determines which type of polygon it is. If relative is true, the polygon is a scalable polygon. If relative is false, the polygon is an absolute coordinate polygon. relative is settable only at polygon creation time. Consequently, once a polygon has been created, it cannot be changed to its other form.

The special attributes for a polygon are defined as follows:

variable 	    default	description
 name		     value

point_list    [(0.5, 0), (1, 1), points of the polygon--must be a list of
               (0, 1), (0.5, 0)] tuples. Each tuple must be a pair of fractions
                                 between 0 and 1 if the polygon is 
				 a scalable polygon or a pair of integers if 
				 the polygon is an absolute coordinate polygon.

relative	true		 determines whether the polygon is a scalable
				 polygon (true) or an absolute coordinate
				 polygon (false). This attribute is settable
				 only at polygon-creation time.

_abs_point_list  read-only	 The absolute coordinates of the polygon. This
				 variable will be the same as the point list
				 for absolute coordinate polygons. This 
				 variable will contain the computed absolute
				 coordinates for scalable polygons.
		

Absolute coordinate polygons.

The specification for an example absolute coordinate polygon might look as follows:

p1 = pam.Polygon(point_list = [(10, 10), (40, 10), (25, 50), (10, 10)],
		 relative = pam.false)

Scalable polygons. A scalable polygon's left, top, width, and height slots are both settable with integer values and constrainable via formulas. The polygon will be appropriately translated and resized in response to changes to any of these variables. The specification for an example scalable polygon might look as follows:

r1 = pam.Rectangle(left = 30, top = 40)

p1 = pam.Polygon(point_list = [(0,0), (1, 0), (0.5, 1), (0, 0)],
		 left = pam.Formula(lambda self: r1.left + r1.width + 20)
		 top = pam.Formula(lambda self: r1.top), 
		 width = 30, 
		 height = 40)

Line

Pam's line object has the following additional attribute slots:

variable 	    default	description
 name		     value

x1		    0		horizontal coordinate of first endpoint
y1		    0		vertical coordinate of first endpoint
x2		    10		horizontal coordinate of second endpoint
y2		    10		vertical coordinate of second endpoint
as_line           true          tells Pam to treat the object like a line
left              read-only     the line's left
top               read-only     the line's top
width             read-only     the line's width
height		  read-only	the line's height
A line object may be created by instancing Line. The position of a line can be set only via the x1, y1, x2 and y2 slots. For example:

my_line = pam.Line(x1 = 20, y1 = 50, x2 = 80, y2 = 130)
is legal, but
my_line = pam.Line(left = 20, top = 50, width = 60, height = 80)
is illegal and will cause an exception.

Arrow_Line

Pam's arrow-line object has the following additional attribute slots:

variable 	    default	description
 name		     value

head_width	    3		width of arrow head
head_length         5		length of each arrow head line
An arrow-line is a line with an "arrow head" at the (x2,y2) end.

Arc

Pam's arc object supports the following additional attributes:

variable 	    default	description
 name		     value

angle1		    0	        origin, degrees from 3 o'clock
angle2		    360	        terminus, distance from origin

An arc object may be created by instancing Arc. For example:

my_arc = pam.Arc(left = 20, top = 50, width = 60, height = 80, 
                 angle1 = 90, angle2 = 100)
The angle1 attribute specifies the origin of the arc, in degrees from 3 o'clock. The angle2 attribute specifies the terminus (angular distance from origin), in degrees. The default for angle1 is 0, and the default for angle2 is 360. Consequently, by leaving out these two attributes, you can create an oval or circle -- but you can also use Pam's oval and circle objects to do this as well (see below).

Oval

An oval object may be created by instancing Oval. No additional slots are supported.

Circle

Pam's circle object supports the following additional attributes:

variable 	    default	description
 name		     value

radius		   read-only	radius of circle
diameter	   10		diameter of circle
as_circle	   true         tells Pam to treat the object like a circle
width              read-only    width of circle
height		   read-only    height of circle
A circle object may be created by instancing Circle. Pam constrains the radius and diameter slots to have the expected relation: radius = 2 * diameter. The width and height attributes are similarly constrained, so that the width and height of the circle are always equal to each other and to the diameter.

The only size slot that can be modified or constrained by the user is the diameter slot. The width, height, and radius slots are all read only.

Roundtangle

A roundtangle is a rectangle with rounded corners. Pam's roundtangle object supports the following additional slot:

variable 	    default		description
 name		     value

radius		    Am_SMALL_RADIUS	radius of semicircle in corner

A roundtangle object may be created by instancing Roundtangle. For example:

my_roundtangle = pam.Roundtangle(left = 20, top = 50, width = 60, height = 80, 
                 radius = 10)
The radius attribute specifies the radius of the half-circle that makes up a corner. You can specify this value using an integer, or the pre-defined constants Am_SMALL_RADIUS, Am_MEDIUM_RADIUS, and Am_LARGE_RADIUS, whose values are determined by the Amulet run-time library (and are therefore subject to change). The default is Am_SMALL_RADIUS.

Bitmap

A bitmap object allows you to display an image in a specified part of the screen. A bitmap object may be created by instancing Bitmap. For example:

my_bitmap = pam.Bitmap(left = 20, top = 50, image = 'mypic.xbm')
The image attribute specifies the file from which the image should be read. The width and height attributes of the bitmap default to the width and height of the entire image. These two attributes are read-only.

Text

A text object allows you to display a text string in a specified part of the screen. A text object has the following attributes:

variable 	 default	description
 name		  value

text		    ''		text string to be displayed
font	       Am_Default_Font  font used to display text string
width	         formula	width of the displayed text string
height	         formula        height of the displayed text string

A text object may be created by instancing Text. For example:

my_text = pam.Text(left = 20, top = 50,
                   text = 'Not on a plane, not on a train')
This code will display the string 'Not on a plane, not on a train' with the lower left corner of the 'N' at location (20, 50), using the default font on your system. If you wish to use a different font, you could create a Font Object and use it to specify the font attribute of your Text Object, as in the following example:

my_font = pam.Font(family = pam.Am_FONT_SERIF, size = pam.Am_FONT_LARGE,
                   bold = pam.true, italic = pam.true)

my_text = pam.Text(left = 20, top = 50, 
                   text = 'Not on a plane, not on a train', 
                   font = my_font)

A font object has the following attributes:

variable 	    default	     description
 name		     value

family      	  Am_FONT_FIXED      font family
size        	  Am_FONT_MEDIUM     font size
bold        	     false	     whether to use bold-face
italic     	     false  	     whether to use italics
underline   	     false           whether to use underline

The family attribute specifies the font family in which to draw the text. The following values are supported :

The actual fonts used, as well as the default font, are determined by your system.

The size attribute specifies the text size to use. The following values are supported :
Again, the actual sizes used depend on what fonts are installed in your system.

Note that the width and height attributes of the text default to the width and height of the entire string. You can display part of the string by setting these attributes to values smaller than the defaults.

Windows

Windows are discussed in the next section with groups.

Grouping Objects

A collection of objects can be grouped by placing them in a Group object. When an instance of the Group class is created, instances of all of the Group's parts are also created.

Instance Variables

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. For example:

class arrowline (pam.Group):
    components = [ ('line',
			lambda: am.Line(x1 = pam.Formula('self.owner.x1'),
			    	    y1 = pam.Formula('self.owner.y1'),
			    	    x2 = pam.Formula('self.owner.x2'),
			    	    y2 = pam.Formula('self.owner.y2'))),
	 	   ('arrow', 
			lambda: pam.Polyline(point_list = 
			   pam.Formula(lambda self: compute_pts(self))))
		 ]

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 line and arrow.

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

Finally, each part also has a name variable that stores the object's name (e.g., 'line' or 'arrow').

Methods

add_part(object, name = None)

add_part dynamically adds parts to a group object after it has been created. The add_part method takes an object and an optional name, which must be a character string, and adds the object as a part of the group. For example:

my_group.add_part(a_rect)
my_group.add_part(a_text, 'label')

If a name is passed to the add_part method, then a reference to the part is placed in a variable with that name in the group object. For example, the latter call in the above example stores a reference to a_text in my_group.label. If a name is passed to the add_part method, the name is also stored in the part's name variable.

Regardless of whether or not a name is passed as an argument, the owner variable in the object is set to its new group. Hence, in the above calls, both a_rect.owner and a_text.owner would be set to my_group.

remove_part(object)

An object can be removed from a group by calling remove_part with the object to be removed. For example, a_rect could be removed from my_group via the call:

my_group.remove_part(a_rect)

When an object is removed from a group, its owner variable is set to FNULL and its name variable, if one is defined, is destroyed. If the part was named, than the variable that pointed to this part in the group is set to FNULL. The reason for storing FNULL in the owner variable and in the group's partname variable rather than destroying these slots is to avoid destroying formulas that depend on these variables. These formulas will be temporarily inactivated. If the object is added to a new group, the formulas that depend on its owner slot will be reactivated. Similarly, if a new part with the same name as the old part is added to the group, the formulas in the group and in the group's parts that refer to this partname will be reactivated. Hence parts can be swapped in and out of a group without altering the formulas that depend on these parts.

An object does not have to be removed from a group before it is destroyed. The destroy method automatically removes an object from a group.

destroy()

destroy recursively destroys all of a group object's parts by calling their destroy commands, and then destroys the group object.

point_in_part(x, y, ref_obj)

point_in_part returns the topmost part (least covered) in the group that contains this point.

Windows and Screens

Window objects can be created by instancing Window. The Window class is a subclass of Group, so windows support all operations that are supported by groups.

In order to display a graphical object in a window, it must be attached to the window using the add_part command. For example:

my_win.add_part(my_group)

In turn, a window must be attached to the screen object using the add_part command:

pam.screen.add_part(my_win)

Although there is a Screen class in Pam, Pam currently supports only one Screen object, called screen. Windows should always be added to this object.

When an object or window is destroyed, it is automatically removed from the window or screen of which it is a part. An object may be removed explicitly from a window using the remove_part method:

my_win.remove_part(my_group)

Similarly, a window can be removed from the screen using the remove_part method:

pam.screen.remove_part(my_win)

Style Objects

Style objects provide a coherent way of setting a grab-bag of attributes in graphical objects. You can either use a pre-defined style object, or create a custom style object of your own.

Pre-defined Style Objects

Pam's pre-defined style objects give you a convenient way of setting an object's attributes to some commonly-used values. These objects fall into three categories: Color, Line-style, and Stipple. Note that the Color styles all have line-thickness zero, and the line-styles have color black. So, for example, if you wanted to create a rectangle with a thick blue line, you'd have to create a custom style, as described in the next section.

Color styles

Am_Red 		Am_Cyan		Am_Motif_Gray		Am_Motif_Light_Gray
Am_Green	Am_Orange	Am_Motif_Blue		Am_Motif_Light_Blue
Am_Blue 	Am_Black	Am_Motif_Green		Am_Motif_Light_Green
Am_Yellow 	Am_White	Am_Motif_Orange		Am_Motif_Light_Orange
Am_Purple			Am_AMulet_Purple	

Line styles

Am_Thin_Line 	Am_Line_1	Am_Line_4	Am_Dashed_Line
Am_Line_0	Am_Line_2	Am_Line_8	Am_Dotted_Line

Stipple styles

Am_Gray_Stipple			Am_Opaque_Gray_Stipple
Am_Light_Gray_Stipple		Am_Diamond_Stipple
Am_Dark_Gray_Stipple		Am_Opaque_Diamond_Stipple
Am_No_Style

Note that you cannot re-define any of the attributes of pre-defined style objects; trying to do so will give you an error. For example, the following code would not work:

myblue = pam.AmBlue
myblue.r = 0.5

User-defined Style Objects

Pam allows you to define your own style attributes, using a user-defined style object. Currently, you can define color using an RGB model with values in the range [0,1] and line-thickness using an integer. Say, for example that you wanted to create a rectangle with a 5-pixels-thick blue line . The following Pam code fragment would do this for you:

thickblue_style = pam.Style(r = 0.0, g = 0.0, b = 1.0, thickness = 5)
my_rect = pam.Rectangle(top = 20, left = 20, width = 100, height = 50,
                        style = thickblue_style)
Unlike graphical objects, whose attributes can be changed after the object is created, user-defined style objects do not allow their attributes to be changed after the object is created. For example, the following Pam code would result in an error:

thickblue_style = pam.Style(r = 0.0, g = 0.0, b = 1.0, thickness = 5)
my_rect = pam.Rectangle(top = 20, left = 20, width = 100, height = 50,
                        style = thickblue_style)
thickblue_style.r = 0.5

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 (discussed in the next section), and then updates the display after each event has been processed.

To get back to the Python interpreter, either:

  1. type Meta-Shift-F1 in the Pam window. (The Pam window is labeled Amulet), or

  2. call pam.exit_event_loop

Interactor Objects

Let's say that you've created a graphical object, like a rectangle or line, and you want to do things with it, like resizing it or moving it around the window in which you've placed it. Pam supports this kind of ability through an Interactor Object. You can think of an Interactor Object as an invisible magic substance that you drop onto a graphical object to give the object the power to interact with the user.

All Pam Interactors support the following slots:

variable 	   default		description
 name		   value

start_when         'LEFT_DOWN'		mouse event that initiates interaction

start_where_test   Am_Inter_In_		position that cursor must be in to 
		   Object_Or_Part	initiate interaction

stop_when	   'LEFT_UP'		mouse event that terminates interaction

active		   true			whether interactor works or not

start_callback     (interactor-specific) routine that gets called when the
                                         interactor starts

running_callback   (interactor-specific) routine that gets called continuously
                                         as the interactor runs

callback	   (interactor-specific) routine that gets called when 
					 the interactor finishes
Further information on the possible values for all these slots can be found in Section 5.3 of the Amulet Reference Manual, and common sense also suggests what these values might be. For example, since the start_when attribute defaults to 'LEFT_DOWN', it makes sense that other values for this attribute would be 'LEFT_UP', 'MIDDLE_DOWN', etc.

The callback functions for each of the interactors have default actions. If you want to override these default actions, you can write your own callback functions. The sections that describe the individual interactors describe which parameters each callback takes. If you want to extend the default callback functions, rather than overriding their behavior, you can call the default callback functions by prefixing them with their superclass name. For example, to call the default start_callback for a move/grow interactor, one would use the name pam.Move_Grow_Interactor.start_callback. There is more information about callbacks in each of the interactor sections.

The Move_Grow_Interactor

As its name suggests, the Move_Grow_Interactor allows you to make a graphical object movable or resizable by the user. Here is an example of using the Move_Grow_Interactor:

my_rect = pam.Rectangle(top = 20, left = 20, width = 100, height = 50)
my_inter = pam.Move_Grow_Interactor()
my_rect.add_part(my_inter)
pam.update()
pam.event_loop()
In this example, we haven't specified values for any of the attribute slots of the interactor, so how do we know what sort of behavior it will support? Fortunately, Pam provides some sensible defaults for the slots: in the example above, the interaction would start when the user clicked the left mouse button inside the rectangle, and would stop when the user released this button. Between those two events, the rectangle would move around its window as the user moved the mouse around. Note the invocation of pam.event_loop(); without this call, no user interaction could take place.

Here are additional attributes that Pam supports for the Move_Grow_Interactor:

variable 	     default		description
 name		     value

growing		     false		if true, mouse movement resizes the
					object; otherwise, it moves it

start_object	     None		the object that will be modified
                                        by this interactor

feedback_object      None               a feedback object that can be used
                                        to show the object's new position or
					size. A formula that reads the 
                                        object in the start_object slot and 
                                        then reads the object's as_line and
                                        as_circle slots can be used to return
					the appropriate feedback object (either
					a line, circle, or rectangular feedback
                                        object).

By default, a Move_Grow_Interactor behaves as follows. When the interactor starts, the start_callback checks the feedback slot of the interactor. If the feedback slot references a feedback object, then this feedback object is made visible and its position and size are set to the position and size of the object being moved or resized. If the value of the feedback slot is None, the start_callback does nothing.

While the move/grow interactor is running, the default running callback sets the appropriate position and size attributes of either the feedback object, if one exists, or the object being modified, if no feedback object exists.

When the move/grow interactor stops, the default callback function moves or resizes the object being modified.

If you want a different behavior for any of these three actions (start, running, or stop), you will need to write your own set of callback functions. All three callbacks take the same set of parameters:

self           : A reference to the interactor

obj_changed    : A reference to either the feedback object or to the
                  object being modified, whichever is appropriate
		  (if there is a feedback object, then the start and
		  running callbacks will get references to the feedback
		  object).

left   (x1)    : The left, top, width, and height values that the
top    (y1)       interactor has computed for a non-line object, or
width  (x2)	  x1, y1, x2, y2 if the object is a line. Unlike Amulet,
height (y2)       if the object is a line, the value will be for the
                  x1, y1, x2, y2 slots, no matter whether the object
		  is being moved or resized (Amulet will return the
		  line's left, top, width, and height if the line is
		  being moved).

For example, to turn the changed object red while it is being moved and then change it back to white when the movement is complete, one could write the following two callbacks:

def start_callback(self, obj_changed, left, top, width, height):
    pam.Move_Grow_Interactor.start_callback(self, obj_changed, left, 
                                             top, width, height)
    obj_changed.fill_style = pam.Am_Red

def callback(self, obj_changed, left, top, width, height):
    pam.Move_Grow_Interactor.callback(self, obj_changed, left, top, 
                                             width, height)
    obj_changed.fill_style = pam.Am_White

The Choice_Interactor

The Choice_Interactor can be employed to allow selection from a group of objects -- for example, items in a list, "radio" buttons, etc. You add the Choice_Interactor to the object that contains these items, as shown in the following example:

win = pam.Window(top = 200, left = 200, width = 400, height = 400)
pam.screen.add_part(win)
square = pam.Rectangle(top = 50, left = 50, width = 50, height = 50,
     selected = pam.false,
     interim_selected = pam.false,
     line_style = pam.Formula(lambda self: (self.interim_selected 
                                     and pam.Am_Line_8) or pam.Am_Line_0),
     fill_style = pam.Formula(lambda self: (self.selected and pam.Am_Red) 
                                           or pam.Am_White))
circle = pam.Arc(top = 150, left = 50, width = 50, height = 50,
     selected = pam.false,
     interim_selected = pam.false,
     line_style = pam.Formula(lambda self: (self.interim_selected and 
                                     pam.Am_Line_8) or pam.Am_Line_0),
     fill_style = pam.Formula(lambda self: (self.selected and pam.Am_Red) 
                                           or pam.Am_White))
inter = pam.Choice_Interactor()
win.add_part(square)
win.add_part(circle)
win.add_part(inter)
pam.update()
pam.event_loop()
In this example, we've put up a small square and circle, and allowed th user to choose between them. The selected item is colored red. As the choice interactor runs, the item currently under the mouse cursor has its line style thickened.

Pam's Choice_Interactor supports the following additional slots:

variable 	     default		description
 name		     value

how_set		    Am_CHOICE_TOGGLE	whether single or multiple
					values will be selected

first_one_only	    false		whether mouse button must be
					released to move between items
					(i.e,. whether menu- or button-like)

value		    None		object(s) selected. Will be either
                                        None or a single object if 
					Am_CHOICE_TOGGLE or Am_CHOICE_SET 
					are used. Will be a list of 0 or more 
					items if Am_CHOICE_LIST_TOGGLE is used.
					Will always be None if Am_CHOICE_CLEAR
					is used.

object_modified    read-only            the object the mouse was over when
                                        the interactor stopped. If how_set
					is either Am_CHOICE_LIST_TOGGLE,
					Am_CHOICE_CLEAR, or Am_CHOICE_TOGGLE,
					this slot can be used to determine
					the item that was affected by this
					interaction.

By default the choice interactor works as follows. As the interactor runs, it sets the interim_selected slot of the object currently under the mouse to true. The interim_selected slots of all other objects are set to false. When the interactor stops, interim_selected slots of all the objects become false. The selected slots of all the objects in the interactor's value slot are set to true. The selected slots of the remaining objects are set to false. If the Am_CHOICE_CLEAR option is set, then the selected slot of the final object the mouse cursor was over is set to false.

Warning: Pam does not define the interim_selected and selected slots as a part of the base class for graphical objects. Hence, you must explicitly define default values for the interim_selected and selected slots in your graphical objects. If you do not explicitly define these slots and your objects access these slots in order to set their properties, then when the objects initially appear on the screen, they will cause an attribute error when they try to consult the interim_selected and selected slots.

The callbacks for the choice interactor have only one parameter:

self           : A reference to the interactor
The value and object_modified slots of the interactor are useful for finding out which objects are currently selected, and which object was affected by this particular interaction.

The Text_Edit_Interactor

Pam allows you to make a Text Object editable, by means of the Text_Edit_Interactor:

text = pam.Text(text = 'Hello World', top = 50, left = 50)
win.add_part(text)
inter = pam.Text_Edit_Interactor()
text.add_part(inter)
pam.update()
pam.event_loop()
By placing the cursor over the edit-enabled text, the user can delete or add characters as in any text editor. The Text_Edit_Interactor supports the following additional attribute slots:

variable 	     default		description
 name		     value

value		     ""			value of text after edit

old_value	     ""			value of text before edit

stop_when            RETURN             default stop event

start_where_test     Am_inter_in_text_  default start where test
                     object_or_part
Note that the defaults for the start_where_test and stop_when slots have different values in the Text_Edit_Interactor than they do in the general interactor: in the Text_Edit_Interactor, start_where_test defaults to Am_inter_in_text_object_or_part, and stop_when defaults to 'RETURN' (i.e., interaction terminates when the user hits a carriage-return.)

As the text edit interactor runs, it continuously updates the text string in the object that it is editing. Consequently, when the text edit interactor stops, the object's text slot will contain its final value.

The callbacks for the text edit interactor have three parameters:

self           : A reference to the interactor

obj_changed    : The object whose text string was modified

value          : The new text string

The New_Points_Interactor

Pam allows you to create a new graphical object interactively, via the New_Points_Interactor. This interactor's behavior is like a more general version of the way that new objects are created in a typical drawing program like MacDraw (tm) or XFig: you move the mouse cursor the point in the window where you want the object to be located, and you click down and drag until you've got the size you want; releasing the mouse creates the object.

The New_Points_Interactor provides you with a little more flexibility, however: You can use any graphical object as the "feedback object" for the click-and-drag phase, and you can create any object once you've released the mouse button. So, for example, you can create a group of circles using a rectangle as the feedback object. Of course, this flexibility requires a little extra work on your part: you must first create the feedback object, and write a creation function, before the interactor can work as desired. Here is a simple example:

def create_rect(self, l, t, w, h):
  new_rect = pam.Rectangle(left=l, top=t, width=w, height=h)
  win.add_part(new_rect)
  return new_rect

feedback_rect = pam.Rectangle(visible=pam.false, 
			      line_style = pam.Am_Dashed_Line)

inter = pam.New_Points_Interactor(feedback_object = feedback_rect,
                                  create_new_object_method = create_rect)
win.add_part(feedback_rect)
win.add_part(inter)
pam.update()
pam.event_loop()
There are a few things worth noting about this example:
  1. The creation function does not necessarily have to return a value, but if you wish to refer to the newly-created object at any point in the future, you must make the creation function return this objbect, which can then be accessed through the value slot of the interactor.

  2. The argument structure of the creation callback is fixed: the first argument is the interactor that created the object, and the remaining four arguments are the object's position and dimensions.

  3. The visible property of the feedback object is set to false, so that the object will not appear until interaction begins.

The New_Points_Interactor supports the following additional slots:

variable		 default	description
 name			 value

feedback_object		 None		object  to show during interaction

as_line			 false		should be set to true if
					feedback object is a line	

flip_if_change_sides	 true		if true, flip object when 
					cursor moves to top or left of
					click-down point

start_where_test	 Am_Inter_In	position that cursor must be in to 
					initiate interaction

create_new_object_method None		function that gets final position and
					dimenions of feedback object and
					whose return value is put into the
					value slot of the interactor

value			 None		value returned by create_new_object_
					function (typically, newly-
					created object)
There are a couple things to note about the Pam new points interactor:

  1. The default for the start_where_test slot has a different value in the New_Points_Interactor than it does in the general interactor. In the New_Points_Interactor start_where_test defaults to Am_inter_in (i.e., the mouse must be inside the object of which the interactor is a part, in order to start the interaction).

  2. The new points interactor always returns two points. If you want to create a behavior in which only one point is required (e.g., the user points at a location on the screen where a fixed sized object should be created), you can do this by not providing a feedback object and only using the first two coordinates returned by the interactor. Since no feedback object is provided, no feedback object will appear. Consequently, the effect of the user pressing a button and releasing is the same as if only one point were entered.

By default, the new points interactor works as follows. When the interactor starts, the start callback makes the feedback object visible, if there is one. As the interactor runs, the running callback continually updates the feedback object's size. When the interactor stops, it calls the create_new_object_method and then the callback function. The create_new_object_method is expected to return a new object, while the callback does any other processing that is required.

The callbacks for the new points interactor have five parameters:

self            : A reference to the interactor

left   (x1)    : The left, top, width, and height values that the
top    (y1)       interactor has computed for a non-line object, or
width  (x2)	  x1, y1, x2, y2 if the object is a line
height (y2)

Widget Objects

Widgets are the little controls -- buttons, menus, and the like -- that let users interact with a program.

All Pam widgets support the following slots:

variable 	      default		description
 name		      value

active		      true		whether widget works or not

active_2	      true		when false, turns off graying-out
				        of widget when widget is
					inactive

fill_style	      Am_Amulet_Purple	color of widget

font		      Am_Default_Font	font to use for item labels

implementation_parent None		widget that will also respond
					this widget's event -- e.g.,
				        dialog box could be
					implementation parent of its 
					'Okay' button

max_rank              false		If max_rank is false, the widget's
                                        items are laid out in a single row
					or column. If max_rank is an integer,
					that integer denotes the maximum 
					number of items that can be placed in
					a row or column

value		      None		current value of widget


The Button_Panel widget

As its name suggests, the Button_Panel widget allows you to create a panel of clickable buttons, each with its own label and callback function. Here are additional the attributes that Pam supports for the Button_Panel widget:

variable 	      default		 description
 name		      value

final_feedback_wanted false		 whether to show which item is
					 selected or not

h_align		      Am_LEFT_ALIGN	 in a vertically-arranged
					 button panel with variable-
				         width buttons, determines
					 how buttons should be
					 arranged in panel (one of
				         Am_LEFT_ALIGN, Am_CENTER_ALIGN,
					 or Am_RIGHT_ALIGN)
		
	
v_align		      Am_CENTER_ALIGN	 works like h_align, but is
					 used only in horizontally-
					 arranged button panels with
					 variable-height  buttons. Possible
					 values are Am_TOP_ALIGN, 
					 Am_CENTER_ALIGN,
					 and Am_BOTTOM_ALIGN.
		

width		      Am_Width_Of_Parts	 read-only

height		      Am_Height_Of_Parts read-only

how_set		      read_only 	 whether single or multiple
					 buttons can be selected

items	              None		 a list of tuples, each  of the form
					 ('label', routine), where
				         label is the button label and
					 routine the callback to
					 execute when the button is clicked
			
item_offset	      3			 number of pixels between
					 button label and button edge

layout		      Am_Vertical_Layout button orientation (either
				         Am_Vertical_Layout or 
					 Am_Horizontal_Layout)


The following code example illustrates a simple usage of the Panel Button. Note that:
  1. it is necessary to invoke pam.event_loop() in order for the buttons to respond to clicks, and

  2. callbacks take self as their single argument

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

def do_pooh(self):
    print 'Oh bother!'

def do_piglet(self):
    print 'I say, wake up, Pooh!'

def do_tigger(self):
    print 'Ta-ta for now!'

list = [('Pooh',do_pooh), ('Piglet',do_piglet), ('Tigger',do_tigger)]

panel = pam.Button_Panel(top=20, left=20, items=list)

win.add_part(panel)
pam.update()
pam.event_loop()

The Menu_Bar widget

The Menu_Bar widget allows you to create a sequence of menus, as typically appears in at the top of the interface in many word-processor programs. Here are additional the attributes that Pam supports for the Menu_Bar widget:

variable 	      default		 description
 name		      value


items	              None		 a list of tuples, each  of the form
                                         ('menu-name',
					 [('lable1',routine1), 
                                          ('label2',routine2), ...])
			
The following code example illustrates a simple usage of the Menu Bar. Note that callbacks take self as their single argument:

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

def do_save(self):
   print 'save'

def do_open(self):
   print 'open'

def do_new(self):
   print 'new'

def do_cut(self):
   print 'cut'

def do_copy(self):
   print 'copy'

def do_paste(self):
   print 'paste'

list = [
        ('File', [('Save',do_save), ('Open',do_open), ('New',do_new)]),
        ('Edit', [('Cut',do_cut), ('Copy',do_copy), ('Paste',do_paste)])
       ]

bar = pam.Menu_Bar(top=20, left=20, items=list)
win.add_part(bar)

pam.update()
pam.event_loop()

The Radio_Button_Panel widget

The Radio_Button_Panel widget allows you to create a panel of labeled, lozenge-shaped buttons, only one of which is typically set at a given time. In this way, you can present a set of mutually-exclusive selections to the user, and allow the user to choose one of them. Here are additional the attributes that Pam supports for the Radio_Button_Panel widget:

variable 	      default		 description
 name		      value


items	              None		 a list of labels

how_set		      read_only 	 whether single or multiple
					 values will be selected

box_width	      15		 width of radio button, in pixels 

box_height	      15		 height of radio button, in pixels

box_on_left	      true	         button to left of text if true,
				         to right of text if false

fixed_width	      false		 ???

final_feedback_wanted true		 whether or not to show which item is
				         selected

callback	       None		 function called when the user selects
                                         an option
			
The following code example illustrates a simple usage of the Radio Button Panel:

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


list = ['WIMZ', 'WNCW', 'WUOT' ]

panel = pam.Radio_Button_Panel(top=20, left=20, items=list)

win.add_part(panel)
pam.update()
pam.event_loop()
After the user selects an item from the panel, the identity of this item can be obtained as panel.value; a string value is returned.

The callback for the radio button panel takes only one parameter:

self           : A reference to the radio button panel
For example:
def my_callback(self):
    print self.value

panel.callback = my_callback
pam.event_loop()

The Checkbox_Panel widget

The Checkbox_Panel widget allows you to create a panel of labled, square buttons, any number of which can be set at a given time. Here are additional the attributes that Pam supports for the Checkbox_Panel widget:

variable 	      default		    description
 name		      value

items	              None		    a list of labels

how_set		      Am_CHOICE_LIST_TOGGLE whether single or multiple
					    values will be selected

box_width	      15		    width of check-box, in pixels 

box_height	      15		    height of check-box, in pixels

box_on_left	      true	            button to left of text if true,
				            to right of text if false

fixed_width	      false		    ???

final_feedback_wanted true		    whether or not to show
                                            which item is selected

callback	       None		    function called when the user 
                                            selects an option
			
			
After the user selects items from the panel, the identity of these items can be obtained as panel.value; a list of strings is returned.

The callback for the radio button panel takes only one parameter:

self           : A reference to the check box panel
For example:
def my_callback(self):
    print self.value

panel.callback = my_callback
pam.event_loop()

The Scrolling_Group widget

The Scrolling_Group widget is useful when you have an item or items that you need to be able to scroll through, because the items take up too much space to be displayed all at once.

Pam supports the following additional slots for the Scrolling_Group widget:

variable 	      default		 description
 name		      value

width			150		object's 
height			150		dimensions

x_offset		0		coordinates of visual region,
y_offset		0		in relation to origin of inner
					region

inner_fill_style	None		background fill color: if None,
				        'fill_style' attribute value
					is used


h_scroll_bar		true		whether or not to show
					horizontal scrollbar


v_scroll_bar		true		whether or not to show 
					vertical scrollbar

h_scroll_bar_on_top	false	        if true, horizontal scrollbar
				        is put on top; otherwise, it's
					put on bottom

v_scroll_bar_on_left	false	        if true, vertical  scrollbar
				        is put on left; otherwise, it's
					put on right

h_small_increment	10		small increments, in pixels, of	 
v_small_increment	10	        scrollbars: determine how much
				        scrolling occurs when user clicks
					on scrollbar arrows

h_large_increment	None	        large increments, in pixels, of
v_large_increment	None	        scrollbars: determine how much
					scrolling occurs when user clicks
					in scroll area next to slider

inner_width		400		size of entire group, not just
inner_height		400		visible portion
	
The following code example illustrates a simple usage of the Scrolling_Group widget:

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

sg = pam.Scrolling_Group(top=20, left=20, width=200, height=200)
win.add_part(sg)

square = pam.Rectangle(top=50, left=50, width=20, height=20)
circle = pam.Circle(top=100, left=250, diameter=30)

sg.add_part(square)
sg.add_part(circle)

pam.update()
pam.event_loop()
Initially, the circle would be outside the viewing range, but could be moved into view by using the horizontal scrollbar.

The Text_Input widget

The Text_Input widget provides an area in which text can be entered and edited by the user. Here are additional attributes that Pam supports for the Text_Input widget:

variable 	    default			description
 name		    value

value		     ''				text in the widget

label		     ''				label to left of text area

width		     150			width of text area

font		     Am_Default_Font		font for editable text

label_font	     Am_Default_Label_Font	font for label

height		     (formula)			set to maximum of label and
						value strings' heights

callback	     (see below)		function called when the user 
						hits a carriage return
					    
The default Text_Input callback function sets the widget's value slot to the text entered by the user. If you want a different behavior for this function, you can write your own callback, which should accept two arguments, the first being the widget itself, and the second being the new value. For example:

def beavis_callback(self, value):
    print 'Uh... you said ' + value + '... huh huh huh!'
	
widget = pam.Text_Input(top=100, left=20, callback=beavis_callback)
win.add_part(widget)
				    


Scroll_Bar widgets

Scroll_Bar widgets provide what is often called a "slider" in the GUI literature; i.e., a tool that allows a value to be set in a continuous, graphical, manner by the user. Pam provides two such widgets, namely, the Vertical_Scroll_Bar and Horizontal_Scroll_Bar; the Scroll_Bar itself is an abstract class that you cannot instantiate directly.

The Scroll_Bar widgets provide the following additional slots:

variable 	    default		description
 name		    value

value		     50			value set by scroll bar

width		     20			for Vertical_Scroll_Bar
		     200		for Horizontal_Scroll_Bar

height		     200		for Vertical_Scroll_Bar
		     20			for Horizontal_Scroll_Bar

value_1		     0			value at top (or left)

value_2		     100		value at bottom (or right)     

small_increment	     1		        change when arrow is clicked  

large_increment	     10		        change when scroll bar is clicked
				        between arrow and indicator

percent_visible	     0.2		size of indicator (range = [0,1])

callback	     (see below)	function called when the user 
					moves the scroll-bar indicator
					    
The default callback function sets the widget's value slot to the value shown by the indicator, as the user changes the value. If you want a different behavior for this function, you can write your own callback, which should accept two arguments, the first being the widget itself, and the second being the new value, as in the Text_Input example above.

On the other hand, it is probably more useful to leave the callback slot alone and use a formula to display or otherwise access the value as it changes. The following example shows how you might do this, by having a Text object that reports the scroll bar's value as it changes:


scrollbar = pam.Vertical_Scroll_Bar(top=20, left=20)

win.add_part(scrollbar)

text = pam.Text(top=20, left=40, source=scrollbar,	
	text=pam.Formula(lambda self: str(self.source.value)))

win.add_part(text)
				    


Extensions

The following classes are extensions to Pam. Unlike the rest of the classes in Pam, these extension classes do not correspond directly to classes in Amulet.

Line_Between_Objects

Line_Between_Objects is a sub-class of Line that constrains a line's endpoints to be the closest two points on two objects.

Line_Between_Objects defines these additional attributes:

variable 	    default	description
 name		     value

object1		    None	object that governs (x1,y1)
object2		    None	object that governs (x2,y2)

You can use a circle-like object (as_circle slot true) or a rectangle-like object (both the as_circle and as_line slots are false) for either of the objects. Group objects are handled properly as long as the as_circle and as_line slots are set properly:

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

object1 = pam.Rectangle(top=200, left=200, width=80, height=50)
object2 = pam.Group(top=100, left=100, width=pam.Formula('self.diameter'),
                     height=pam.Formula('self.diameter'),
                     diameter = 40, as_circle = pam.true)

circle = pam.Circle(top=0, left=0, 
                    diameter = pam.Formula('self.owner.diameter'))
object2.add_part(circle)

line = pam.Line_Between_Objects(object1=object1, object2=object2)

win.add_part(object1)
win.add_part(object2)
win.add_part(line)

inter = pam.Move_Grow_Interactor()  # no fun unless we can move objects!
win.add_part(inter)
pam.event_loop()

Arrow_Line_Between_Objects

This is a sub-class of both Line_Between_Objects and Arrow_Line, so that you can draw an arrow between two objects.

Automatic Redisplay

When an application is running Pam's event loop, the display is automatically updated after each event. However, the programmer can manually force an update to occur using Pam's update() command. This command is most useful when the programmer is using the Python interpreter and wants to update the display. For some reason, if there are graphical objects attached to the window the first time update is called, the graphical objects do not appear. Hence, when using update from the interpreter, it is best to call update immediately after creating a window. Thereafter it will correctly redisplay objects.