-- COGDrawing.mesa: Viewers for geometrical drawings
-- last modified by Stolfi - October 16, 1982 6:35 pm
-- To do: add bounding boxes to objects!
-- To do: implement partial repainting with given bounding box
-- compile COGDrawing
DIRECTORY
Graphics USING [Path, Color, Context, black, white],
Rope USING [ROPE],
ViewerClasses USING [Viewer],
Menus USING [MouseButton, MenuLine],
COGCart USING [Box, ScaleFactors],
COGHomo USING [Point];
COGDrawing: CEDAR DEFINITIONS
= BEGIN OPEN Cart: COGCart, Homo: COGHomo, Rope;
-- VIEWER CREATION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Drawing:
TYPE = ViewerClasses.Viewer;
-- A Drawing is a viewer for displaying a dynamically varying collection of dynamically varying Objects in a dynamically varying viewer (so that you can get dynamically varying bugs). The objects may be points, lines, polygons, or any client-defined entities (e.g., Delaunay diagrams). Objects added to the Drawing are remembered, so that the entire Drawing can be repainted when the viewer is moved, and an individual object can be easily erased or modified, without losing the others.
-- The implementation makes it rather easy to keep the drawing scaled so as to fit into the curr viewer window, with automatic rescaling and repainting when the latter changes. Forthcoming is a facility for "blowing up" a selected portion of the Viewer Client-defined objects can use all primitives of CedarGraphics.
-- Besides the standard menu items (Grow, Destroy, <-->, Close), the client can attach to the viewer other procedures to be called when specific menu items are selected, or when specific mouse buttons are clicked.
-- In addition to the object list, a Drawing posesses a list of client-defined properties, which may be used to store data that is global to all objects but local to the Drawing. For example, this allows menu and mouse procedures to communicate with each other and with the procedures that paint objects on the screen. Properties consist of pairs (key, value) where the key is an atom and the value a REF ANY.
-- A Drawing also implements an interlocking mechanism for synchronizing the painting processes, the menu and mouse routines (that are called as forked processes) and the client program. This mechanism is implemented by the Acquire and Release procedures; it is somewhat awkward, but the standard MESA LOCKs aren't much easier to use, would not give complete protection, and would create narrower bottlenecks.
MakeDrawing:
PROC [name:
ROPE, box: Cart.Box]
RETURNS [dr: Drawing];
-- Creates a new Drawing, initially with no Objects. The box is used to define the scale factors used for painting the objects.
-- The initial menu contains the basic viewer entries (Close, Grow, <-->, Destroy) plus a Repaint entry that causes the whole object list to be repainted.
Alive:
PROC [dr: Drawing]
RETURNS [
BOOL];
-- Returns FALSE if the Drawing's viewer has been destroyed for any reason.
-- PROCESS SYNCHRONIZATION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Access: TYPE = {read, write, none}; -- Possible access rights.
Change: TYPE [1]; -- Records a change in access rights for later undoing.
noChange:
READONLY Change;
-- A null change in access rights
-- Before examining any of the Drawing's contents (its property list, the object list, the objects themselves, the mouse action table, or mouse/menu client data), a process P must first get "read access rights" to the drawing, in order to prevent other processes from modifying the viever data under the feet of P. Moreover, if P itself is going to modify the Drawing's contents, it must first get "write access rights" to it, so that others are prevented from reading it until P is done with the changes.
GetReadRights: PROC [dr: Drawing] RETURNS [c: Change];
GetWriteRights: PROC [dr: Drawing] RETURNS [c: Change];
GetRights:
PROC [dr: Drawing, wants: Access]
RETURNS [c: Change] =
INLINE
BEGIN
RETURN
[SELECT wants FROM
read => GetReadRights[dr],
write => GetWriteRights[dr]
ENDCASE => noChange]
END;
-- At any instant, the Drawing is (a) completely available, (b) acquired by one or more processes for reading, or (c) acquired by exactly one process for writing. GetWriteRights [dr] will wait until the Drawing becomes completely available; GetReadRights[dr] will wait until no one is modifying it.
-- Even if P already holds read or write access rights to dr, it is OK to call any of the two procedures above. Their effect is cumulative, so, for example, if P already has read rights, calling GetWriteRights[dr] will wait until allother processes release dr, and then will upgrade P's rights to write. Conversely, if P already has read or write rights, calling GetReadRights[dr] is a no-op.
-- The result of these two procedures is a record of the change made in the access rights of the calling process relative to the drawing dr. This record is used with Release (see below) to undo the change and revert to previously held access rights. For every invocation c ← GetReadRights[dr] or c ← GetWriteRights[dr], a process P must execute a corresponding call Release[dr, c], and these calls must be nested in the proper last-in-first-out order; otherwise chaos and deadlocks will result. Therefore, client procedures that acquire any access rights should catch UNWIND signals and perform the appropriate Release before exiting through ANY path.
-- The switching from the current rights to the ones wanted is indivisible, so other modifying processes cannot sneak in while switching from read to write or vice-versa. However, note that switching from read to write may cause a deadlock if another process with read rights decides to do the same; for this reason, this switching should be avoided as much as possible. Threfore, clients should consider acquiring write rights from the start, instead of read, whenever there is a chance that such switching would be required later on.
Release:
PROC [dr: Drawing, c: Change];
-- Release[dr, c] restores the access rights of the calling process to what they weere before the change c (which MUST be the result of the last unReleased call to GetReadRights[dr, c] or GetWriteRights[dr, c].
-- GRAPHIC OBJECTS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Color: TYPE = Graphics.Color;
black: Color = Graphics.black;
white: Color = Graphics.white;
Size: TYPE = NAT [0..256); -- client-defined object size/width/thickness/etc.
Style: TYPE = NAT [0..256); -- client-defined object style.
PaintOrder: TYPE = INTEGER [0..6]; -- all 0's are painted first, then all 1's, etc.
ObjectParms: TYPE = RECORD
[data: REF ANY, -- kind, coordinates &c of graphic object
color: Color ← black,
size: Size ← 0,
style: Style ← 0
];
ObjectRec: TYPE = RECORD
[Painter: PainterProc, -- a procedure that knows how to paint the object
parms: ObjectParms, -- kind, coordinates, color, style, &c of graphic object
order: PRIVATE PaintOrder, -- used when repainting everything from scratch
pred, succ: PRIVATE Object, -- Objects are connected in doubly linked lists for each order
dr: -- READONLY -- Drawing -- to which this object currently belongs (or NIL if none)
];
Object: TYPE = REF ObjectRec;
PainterProc:
TYPE =
PROC
[dr: Drawing, context: Graphics.Context, sf: Cart.ScaleFactors, parms: ObjectParms];
-- The Painter of each object is called to paint it on the given context. The interpretation of color, size, style and data is left to the procedure. However, note that to erase an object from the screen the Drawing manager sets its color to white and then calls the corresponding Painter. (The color, size and style fields should be part of the data, but in many applications it is easier to have them as separate variables. For example, the data may point to an edge of a graph structure that is being debugged; .
-- The context passed to the Painter is the raw viewer's context, maybe clipped but not scaled; the scale factors sf are such that the Drawing's bounding box (as specified to MakeDrawing) would scale to the context's bounding box, minus a modest margin all around. The Painter should use Cart.Scale to convert object coordinates before calling CedarGraphics. (The context could be scaled once before calling all Painters, but sometimes the dimensions of painted objects (e.g., line thickness) are given in device units).
-- The Drawing package guarantees (I hope) that when a Painter is called, the process doing the call holds at least read access to the Drawing.
-- To modify the parameters of an existing object obj and get the change to be manifested on the screen, the client has basically four options:
-- (0) acquire write rights to the drawing, modify the parameters of the obj, release the rights, and do a RepaintAll (or have some independent process do it periodically). This may be too time-consuming for complex Drawings.
-- (1) acquire write rights to the drawing, call DoErase [obj], modify its parameters, call DoPaint [obj], and release the rights. This may be reasonable in a complex procedure that has to acquire the write rights anyhow, and/or modifies several objects simultaneously.
-- (2) call Remove [obj], modify its parameters, and Add it back again. The caller must be sure that the obj is still in the Drawing's list by the time the Remove is done (otherwise he may get an error); in the extreme cases, the caller may have tohold read access (at least) continuously in the interval between the Add and the Remove.
-- (3) write an ObjectActionProc P that does the modification, and call ModifyObject[obj, P]. This particularly useful in conjunction with ModifyAll.
-- Note that SetStyle is the simplest way to change only the color, size and/or style of an object.
Make:
PROC [Painter: PainterProc, parms: ObjectParms]
RETURNS [obj: Object];
-- Allocates a new ObjectRec obj with given parameters and painting procedure.
Add:
PROC [dr: Drawing, obj: Object, order: PaintOrder ← 3];
-- Inserts obj in the object list of the Drawing dr, in the specified painting order, and calls obj.Painter to draw it on the screen.
-- The order is used only when repainting the all objects from scratch; the painting invoked by Add, and all individual repaintings, are done on top of the existing image. Add will acquire write rights while adding the new object.
-- If the object is already in the Drawing dr, the action will be a no-op. If it is in some other Drawing, an error will be generated.
Remove:
PROC [obj: Object];
-- Erases the indicated object (by painting it with color=white) and removes it from the list of objects. If the object is not in any Drawing, the operation is a no-op. Remove will acquire write rights to the drawing while deleting the object.
RemoveLayers:
PROC [dr: Drawing, minOrder, maxOrder: PaintOrder];
-- Removes all objects in the specified range of painting orders, erasing them by painting them white (if all objects are removed, will erase the screen in a swoop, instead of object-by-object). Will acquire write rights in order to do this.
RemoveAll:
PROC [dr: Drawing] =
INLINE
-- Removes all objects from the drawing, and paints the screen white. Will acquire write rights in order to do this.
{RemoveLayers [dr: dr, minOrder: FIRST[PaintOrder], maxOrder: LAST[PaintOrder]]};
RepaintAll:
PROC [dr: Drawing];
-- Erases everything (by painting the whole viewer white) and repaints all objects, in the specified order. Will acquire read access for the painting.
SetStyle:
PROC
[obj: Object, color: Color ← black, size: Size ← 1, style: Style ← 0];
-- "Erases" the object (by repainting it in white), then sets its color, style and size as indicated, and repaints it on top of the existing image. Will acquire write access to the drawing before doing the change.
Modify:
PROC
[obj: Object, Action: ObjectAction, actionData:
REF ←
NIL,
erase, repaint:
BOOL ←
TRUE]
RETURNS [actionResult:
REF];
-- Computes actionResult ← Action [dr, obj, actionData]. If erase is true, the object is first "erased" (by painting it white); if repaint is TRUE, the object is repainted after the modification. Will acquire write rights to the drawing in order to do the Action.
ModifyAll:
PROC
[dr: Drawing, Action: ObjectAction, actionData:
REF ←
NIL, erase, repaint:
BOOL ←
TRUE]
RETURNS [actionResult:
REF];
-- Performs actionData ← Action[dr, obj, actionData] for each object in the drawing dr (in the paint order), and then returns actionResult ← actionData. If erase is true, each obj is erased (by painting it white) before the Action; if repaint is TRUE, each object is repainted after it. ModifyAll will acquire write rights, and will hold them continously for the whole process.
Enumerate:
PROC [dr: Drawing, Action: ObjectAction, actionData:
REF ←
NIL]
RETURNS [actionResult:
REF];
-- Performs actionData ← Action[dr, obj, actionData] for each object in the drawing dr, and then returns actionResult ← actionData. The Action should not write the objects, only look at them; no repainting takes place, and Enumerate will acquire only read access rights to dr.
ObjectAction:
TYPE =
PROC [obj: Object, actionData:
REF]
RETURNS [actionResult:
REF ←
NIL];
-- ObjectActions are called by Modify and ModifyAll with write access to the object's drawing, and by Enumerate with (at least) read access.
-- LOW-LEVEL OBJECT DRAWING - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- The following low-level procedures may be used to erase and repaint objects. However, the caller must acquire the necessary access right to the Drawing by himself, before calling these procedures.
DoErase:
PROC [dr: Drawing, obj: Object];
-- "Erases" the indicated object by painting it with white color. The object is neither added to nor deleted from the Drawing, and its color field is not changed, so subsequent paintings (which are inhibited while the caller holds write rights) will restore the original drawing appearance. The caller must have write access rights to the Drawing.
DoPaint:
PROC [dr: Drawing, obj: Object];
-- Repaints the indicated object on top of the existing image. The object is neither added nor deleted from the Drawing's list, so, if the object is not in it, the original image may be restored when the whole Drawing is repainted. The caller must have at least read access to the Drawing.
DoPaintAll:
PROC [dr: Drawing];
-- Erases the screen and repaints all objects in the correct paint order. The caller must have at least read access to the Drawing.
-- HELPFUL EXTENSIONS TO CEDARGRAPHICS - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- The following procedures are extensions to CedarGraphics that use homogeneous coordinates and perform automatic scaling by the given scale factors. They are useful in the coding of PainterProcs.
DrawDot:
PROC
[context: Graphics.Context, sf: Cart.ScaleFactors,
pt: Homo.Point, side: Size ← 0, color: Color ← black];
-- Draws a small square of given side (in pixels, independent of the scale factors) centered at pt.
DrawSegment:
PROC
[context: Graphics.Context, sf: Cart.ScaleFactors,
org, dest: Homo.Point, width: Size ← 1, color: Color ← black];
-- Draws a line segment of given width (in pixels, independent of the scale factors) from org to dest. If width = 0 or 1, the CedarGraphics "thin line width" is used.
-- org and dest should not be both infinite.
SetCP:
PROC
[context: Graphics.Context, sf: Cart.ScaleFactors, pt: Homo.Point];
-- Sets the Current Position of the context to point pt.
DrawTo:
PROC
[context: Graphics.Context, sf: Cart.ScaleFactors, pt: Homo.Point];
-- Draws a thin line to point pt.
MoveTo:
PROC
[path: Graphics.Path, sf: Cart.ScaleFactors, pt: Homo.Point];
-- Flushes the given path and sets its alast point to pt.
LineTo:
PROC
[path: Graphics.Path, sf: Cart.ScaleFactors, pt: Homo.Point];
-- Extends the given path to the point pt by a straight line segment.
-- MENU AND MOUSE PROCEDURES - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MouseButton: TYPE = Menus.MouseButton; -- {red, yellow, blue}
EventKind: TYPE = {down, move, up}; -- move means mouse moves with button down.
MouseEvent: TYPE = RECORD
[button: MouseButton,
shift, ctrl: BOOL ← FALSE,
kind: EventKind ← down];
MenuProc: TYPE = PROC
[dr: Drawing, menuData: REF ANY, button: MouseButton];
MouseProc:
TYPE =
PROC
[dr: Drawing, eventData:
REF
ANY, x, y:
REAL, event: MouseEvent];
-- MenuProcs and MouseProcs may be attached respectively to client-defined menu entries or to specific mouse events in the Drawing's viewer. To such events, the client may also attach an arbitrary data record, which is passed to the proc together with the Drawing's state record.
AddMenuAction:
PROC
[dr: Drawing, name:
ROPE, Proc: MenuProc, menuData:
REF ←
NIL,
needs: Access, line: Menus.MenuLine ← 1];
-- Adds a new menu entry with given name, Proc and menuData. After this call, whenever the menu entry is activated by the user, the Proc will be called by a forked process with the specified access rights already acquired, and these will be automatically released upon exit of the Proc.
AddPropertyButton:
PROC [dr: Drawing, key:
ATOM, line: Menus.MenuLine ← 1];
-- Adds a new menu entry with name = Atom.GetPName[key].
-- When the user clicks such entry with the left mouse button, the key property of the drawing is set to $TRUE; when the right button is used, the key property is set to NIL. The entire drawing is repainted in each case.
AddMouseAction:
PROC
[dr: Drawing, event: MouseEvent, Proc: MouseProc, eventData:
REF ←
NIL, needs: Access];
-- Enables the specified mouse event, attaching to it the given Proc and eventData. After this call, whenever the specified event occurs in the window of dr, the Proc will be called with the specified access rights already acquired, and they will be automatically released upon exit.
-- PROPERTY LIST - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
GetProp:
PROC [dr: Drawing, key:
ATOM]
RETURNS [value:
REF];
-- Returns the value of the specified property from the drawing's property list. If the property is missing, returns NIL. Will acquire read access rights (at least) to do this.
PutProp:
PROC [dr: Drawing, key:
ATOM, value:
REF];
-- Replaces the value of the specified property by the given value. If the value is NIL, the property is removed from the list. If the property is missing, it is added to the list. Will acquire write access rights to do this.
END.