How To’s
How To Use the How To’s
The How To’s section is designed to provide specific guidance for specific problems in GF/ST. Graphic objects, tools, handles, events, and so on are all so interconnected, you just can’t explain one part without paying any attention to the others. For this reason, you will find many cross-references between the How To’s and the actual GF/ST hierarchy and methods.
Use the How To’s as a way to get unstuck when you’re stuck. Use them to double-check your own decision-making as you build a GF/ST application. They are not designed for front-to-back reading, but as a reference source.
Releasing Resources
VisualSmalltalk
VA Smalltalk users: welcome to the border between Smalltalk and host resources. Traditionally in these latter two environments, if you use an object that allocates host resources of any kind, you must explicitly release those objects when you are done with them. If you don’t, the Smalltalk garbage collector will collect them when they are no longer referenced, but the resources will not be freed. The result is that your limited resources under your host windowing system will slowly degrade.
Knowing When to Let Go of a GO
One way to do deal with the problem is simply to send the message release to each GO when it is no longer referenced and will be garbage-collected, typically before you close your window or when you delete a GO.
In GF/ST under VA Smalltalk, we have chosen to make use of finalization the default when you create new instances of GFGraphicObject.
We implement a default finalize in GFGraphicObject that says:
self release.
We can therefore be sure that every nodeGO will receive the message finalize, resulting in the release of host window system resources, when it is no longer held onto by anybody.
Mostly GFGraphicObject-Related How To’s
This section summarizes the most common ways to create GO’s and manipulate them.
Creating Simple Figure GO’s
There are numerous class-specific instantiation messages which are easy to find on the class side of the GFGraphicObject hierarchy. Some of them are listed below for your convenience, with appropriate arguments supplied.
GFRectangleGO rectangle: (0@0 extent: 25@25).
GFEllipseGO ellipse: (0@0 extent: 25@25).
GFLineGO start: 0@0 stop: 25@25.
GFRoundedRectangleGO
rectangle: (0@0 extent: 25@25)
cornerEllipse: 5@5.
GFPolylineGO withPoints:
(Array with: 0@0 with: 12@0 with: 25@25).
A Bezier curve is defined by its start and stop points and two control points used to determine the tangents to the start and stop points. The class-specific instantiation methods let you define only the start and stop points, or you can specify the control points also. The arguments for the second method shown below illustrate what the default behavior is when the first method is invoked (i.e., the control points are 10@10 away from the start and stop points).
GFBezierGO start: 0@0 stop: 25@25.
GFBezierGO
start: 0@0
stop: 25@25
controlPoint1: 10@10
controlPoint2: 35@35.
The orthogonal GO’s have a multitude of instantiation methods for governing which start and stop directions they will use. They also have protocol for specifying the start/stop positions together with the direction for each:
GFOrthogonalPathGO bottomStart: 0@0 topStop: 25@25.
A few more simple GO instantiation methods:
GFSplineGO controlPoints:
(Array with: 0@0 with: 12@0 with: 25@25).
GFTextGO text: ‘GF/ST’.
Creating Group and Composite GO’s
You can create group and composite GO’s using the
graphicObjects: method, supplying a collection of any GO’s as an argument. The following script illustrates the process. Also, refer to
Creating a GO in the Network Editor as an example of a simple GFGroupGO.
| aGO goCollection |
aGO := GFEllipseGO ellipse: (0@0 extent: 25@25).
goCollection := OrderedCollection new.
goCollection add: aGO.
goCollection add:
(GFRectangleGO rectangle: (0@0 extent: 25@25)).
goCollection add:
(GFButtonGO fromGO: aGO).
goCollection add:
(GFImageGO origin: 0@0 image: Bitmap fromUser).
GFGroupGO graphicObjects: goCollection.
“ or... GFCompositeGO graphicObjects: goCollection.”
Creating Locators for Dependent GO’s
Locators are required when you use dependent GO’s. Dependent line type GO’s we supply include GFDependentLineGO and GFDependentOrthogonalPathGO. You may use locators if you specialize handle behavior, too. See the discussions in
GFLocator and
Specializing Handles in the Network Editor . For another example of dependent line usage, see the PsiSolid class used in the 3-D Figures demo, discussed in
Dependent Lines.
Assuming we create a GFPolylineGO as follows:
| aGO locator |
aGO := GFPolylineGO
withPoints: (Array with: 0@0 with: 12@0 with: 25@25).
then, we can create a GFLocator for the center of the polyline as:
locator := GFLocator on: aGO at: #center.
When you send the locator the message evaluate or asPoint, it will return the result of sending the message center to the polyline, aGO. Any message understood by the polyline is acceptable, as long as it returns a Point.
Other variations include:
locator := GFLocator on: aGO at: #offCenter: with: 5@5.
The argument 5@5 will be sent as the argument to offCenter:.
locator := GFLocator on: aGO at: #point: with: 1.
This last expression will locate the first point in the polyline. It would only be useful with polylines, because all GO’s do not understand point:. This particular locator would be used to specify the location of a handle at a particular point on a polyline.
There is a set of common protocol in GFGraphicObject to return points at specific locations in GO’s. Specific GO’s extend or override the protocol, as in the point: method of GFPolylineGO. Common protocol that all GO’s understand includes:
•bottomCenter
•bottomLeft
•bottomRight
•center
•leftCenter
•offCenter:
•offCorner:
•offOrigin:
•offTopLeft:
•rightCenter
•topCenter
•topLeft
•topRight
Creating Dependent GO’s
This script illustrates how to create dependent GO’s. The protocols apply to the following classes:
•GFDependentLineGO
•GFDependentOrthogonalPathGO
Note that dependent GO’s use the GFLocator to specify their start and stop locations. The locator is a simple way to forward a message to another object to obtain a point, and it is described in
GFLocator in
Basic Concepts and Classes.
| rect1 rect2 startLocator stopLocator |
rect1 := GFRectangleGO rectangle: (0@0 extent: 25@25).
rect2 := GFRectangleGO rectangle: (30@30 extent: 25@25).
“Sending #asPoint or #evaluate to the following
Locators sends the message #center to the rectangles,
which return the Point location of their center”
startLocator := GFLocator on: rect1 at: #center.
stopLocator := GFLocator on: rect2 at: #center.
“Dependent lines and orthogonal paths take locators
as their start and stop points”
GFDependentLineGO
startLocation: startLocator
stopLocation: stopLocator.
“Orthogonal paths have default values for directions; i.e.,
should they start going left, right, up, or down”
GFDependentOrthogonalPathGO
startLocation: startLocator
stopLocation: stopLocator.
Setting the Menu of a GO
Assume that self returns a menu when sent ellipseMenu and rectMenu. You can attach these menus to the GO’s via the #getMenu event which is triggered by the GO when you click the right mouse button while the cursor is over the GO.
| go1 go2 |
go1 := GFEllipseGO ellipse: (0@0 extent: 25@25).
go2 := GFRectangleGO rectangle: (0@0 extent: 25@25).
go1 when: #getMenu send: #ellipseMenu to: self.
go2 when: #getMenu send: #rectMenu to: self.
Creating a GFDrawingPane to Add to a Window
When you use GF/ST in an application, you will be using a GFDrawingPane, or its double-buffered equivalent GFOwnDCDrawingPane. Typically, though, you will not be sending messages to the GFDrawingPane itself, but to the GFDrawingInterface to which it is attached. The GFDrawingInterface is your main channel of communication to the GO’s as a group. One of the first jobs you need to perform when you create your application is to create an instance of a GFDrawingPane and connect it to an instance of a GFDrawingInterface. The script below shows one convenient way to accomplish the task. Refer to the open and createViews methods in the various applications and examples that come with GF/ST for other specific examples.
| interface drawingPane |
“Step 1: Create the drawing pane and the interface”
drawingPane := GFOwnDCDrawingPane new.
interface := GFDrawingInterface newWithDrawing.
“Step 2: Set it to be the drawingPane’s interface before opening the window”
drawingPane setInterface: interface.
The drawingPane can be added as a subpane in the creation of a window. In practice, the GFDrawingInterface referred to above as interface will be needed again and should be put in an instance variable.
When using an interface builder like WindowBuilder Pro or PARTS, you should create and connect the GFDrawingInterface to the GFDrawingPane before the window opens. In these cases, you will not want to invade the createViews method which WindowBuilder Pro automatically creates for you. Instead, you should use the preInitWindow method in WindowBuilder Pro or the initializeParts method in PARTS to accomplish the task in these cases.
Assume you are using WindowBuilder Pro and that you have named the GFOwnDCDrawingPane ‘drawingPane’. You have also decided to keep the GFDrawingInterface in an instance variable called interface. Your preInitWindow method would look like this:
preInitWindow
“Perform pre-opening initialization”
interface := GFDrawingInterface newWithDrawing.
(self paneAt: ‘drawing’) setInterface: interface
Assume you are using PARTS as an interface builder only. In that case, you would use the properties dialog to name the GFOwnDCDrawingPane #drawing. Your initializeParts method would look similar:
initializeParts
“Perform pre-opening initialization”
interface := GFDrawingInterface newWithDrawing.
(self partNamed: #drawing) setInterface: interface
Setting the Menu of a Drawing
We expect that the default menu for a drawing will suit your purposes during development. The likelihood of it being correct for your end-users is approaching zero, however. It is very simple to reassign the menu via the #getMenu event. You will probably be holding onto the GFDrawingInterface in an instance variable. From it, you can reach the drawing itself, to hook into the #getMenu event:
interface drawing
when: #getMenu send: #mainMenu to: self.
In most cases, you would want to set the menu for the drawing before opening the window. As discussed in
Creating a GFDrawingPane to Add to a Window, appropriate spot would be in
preInitWindow for WindowBuilder Pro or in
initializeParts for PARTS.
It is also easy to completely disable the drawing’s menu:
interface drawing disableMenu.
Locking down a GO
You may want to disable all menus and handles on a GO. For example, the GFButtonGO’s in the GFDemoLauncher are an example of GO’s used in such a manner.
aGO disableInteraction.
Adding and Removing a GO
The simplest way to add and remove GO’s is to use the GFDrawingInterface. The following methods cause the GO to appear on the display or be removed from the display.
interface addGO: aGO.
interface removeGO: aGO.
To add or remove GO’s without redrawing, but still tracking damaged areas for subsequent redraw.
interface quietlyAddGO: aGO.
interface quietlyRemoveGO: aGO.
Once you have finished “quietly” adding and removing GO’s, you can tell the interface to redraw itself:
interface redraw.
Manipulating GO’s
Your application may call for users to manipulate and position GO’s directly. There are, however, many ways to manipulate GO’s programmatically. Some common protocols are summarized below.
To set the origin of a GO to the center of the visible area on the screen without causing a redraw:
aGO origin: aGO interface visibleRectangle center.
To set the origin of a GO to the middle of the drawing without causing a redraw.
aGO origin: interface boundingBox center.
To move a GO to the middle of the visible area on the screen without causing a redraw. The argument, aPoint, specifies the delta in the Smalltalk platform coordinate system.
aGO translateBy: aPoint.
To send a GO to the back or bring it to the front, causing a redraw:
interface sendToBack: aGO.
interface bringToFront: aGO.
To redraw a GO after having modified it in some way, perhaps via translateBy:.
aGO redraw.
To redraw all damaged areas in an interface after modifying GO’s in it, hiding handles first:
interface redraw.
To select a GO:
interface selections: (OrderedCollection with: aGO).
To change the scale of a drawing, causing a redraw. The argument, aPercent, is 100 for 1:1 display.
interface scale: aPercent.
Using GO’s at Line Ends
This release of GF/ST includes the ability to “plug in” any GO at the end of line-type GO’s. By line-type GO’s, we mean subclasses of GFDirectedPathGO. Probably the simplest way to understand the usage of end plugs is to evaluate one of the example methods provided in GFDrawingInterface:
GFDrawingInterface rectangleLineEndExample.
GFDrawingInterface circleLineEndExample.
GFDrawingInterface triangleLineEndExample.
Each of these methods opens up the Drawing Editor on a GFPolylineGO, with different kinds GO’s used at the end. The methods also use GF/ST to display the important instance variables that affect the display and behavior of the GO.
Look in the method body to see how to specify the GO end plug. You’ll be using an instance of GFEndPlugGO. There are three instance variables that you should specify:
•displayPoint - The displayPoint is the point where the line end is located. It will normally be the start or stop point of the line for which the plug is going to be used. abt.ini
•displayOffset - The displayOffset is the offset from the displayPoint to the origin of the GO that is being used as the end plug.
•connectionOffset - The connectionOffset is the offset from the displayPoint to where the line should start drawing itself from.
You can also specify an arbitrary “action” to be performed, instead of a GO as an end plug. When you do so, you will be using a GFEvaluableActionEndPlug. We use a GFEvaluableActionEndPlug for arrow heads on the ends of lines. It would be inappropriate to use a GO, since the arrow head needs to rotate to the angle of the line. To see how we use these kinds of end plugs, look in the GFDirectedPathGO instance method endArrowPlug.
GFEvaluableActionEndPlugs specify a “draw action” and a “shape action”. The draw action draws the end plug, and the shape action draws the mask for the end plug. The mask is black and white, where black represents the opaque area, and white represents the transparent area. It is often more convenient to create instances of GFMessage (an “action“, since it is a subclass of EvaluableAction) and send them the message asGFEndPlug; for example:
(GFMessage new
receiver: self;
selector: #displayEndArrowWith:for:) asGFEndPlug.
GFDirectedPathGO’s use the standard displayWith: message to display themselves and their end plugs (if the end plugs are not nil). In the above example, the result is (assuming self is aLineGO):
aLineGO displayEndArrowWith: aPen for: anEndPlug.
The end plug is passed as an argument so that aLineGO can let anEndPlug know what its displayPoint, displayOffset, and connectionOffset are.
Miscellaneous Topics
Coordinate Systems and Scaling
The coordinate system used by a drawing is the native coordinate system used by the host platform. Internally, GF/ST uses the native coordinate system; however, protocol is provided to convert native coordinate points to user defined coordinates, and vice versa. For example:
nativePoint := aGO coordinateToNative: userCoordPoint.
userCoordPoint := aGO nativeToCoordinate: nativePoint.
It is your responsibility, however, to remember which point is in defined in which coordinate system.
It is sometimes useful to have the origin of a drawing somewhere other than where the native coordinate system places it, and have the x and y axes increase in different directions. The GFCoordinateControl class allows the user to specify where the origin is (relative to the native coordinate system), and in which directions the x and y axes increase. Note that the coordinate system transformation has nothing to do with units of measurement - it only reflects where the origin is, and in what direction the x and y axes increase.
You can specify what form the units take by using the various subclasses of GFLogicalUnit. The default unit specified is in pels (i.e., pixels on the screen). You can also specify either hundredths of millimeters, thousandths of inches, or twips, using the GFHundredthMillimeter, GFThousandthInch, or GFTwip classes. A twip is 1/1440th of an inch and is a common measurement used in print layout.
The GFCoordinateControl, used by the GFDrawing, holds onto the instance of GFLogicalUnit which is active. The GFLogicalUnit, in turn, holds onto a resolution value. In GF/ST, we use the GFThousandthInch logical unit defined by the class method defaultResolution. The defaultResolution method ends up setting the resolution to:
1000.0 @ 1000.0 / Display pen pixelsPerInch.
On one machine at 800x600 resolution, this expression results in resolution being set to 15.625@15.625. In other words, 0.015625 inches per pixel.
If you want to change the logical units for your drawing, you must set up the proper resolution values before the drawing is hooked to the interface. This approach may change in future releases as we provide easier-to-use interfaces to the coordinate system and logical unit framework.
Quad Trees
Typically, you should view the Quad Tree implementation in GF/ST as having “no use serviceable parts.” If you break it, very bad things can happen to your drawing. It is however, both interesting and informative to be able to view the Quad Tree layout in a complex drawing. To see it, send the message showQuad to the drawing interface. Probably the easiest way to accomplish this is to open an inspector on a graphic object and evaluate:
self interface showQuad
The screen displayed here shows the quadrants of a complex drawing in heavy regular (red, if you’re looking at the Help file) lines. The display is a graphic illustration that the search for “what GO is under my cursor” is dramatically reduced in complexity when you only have to deal with one quadrant at a time.
A side note: one of the key responsibilities of the
damageDuring: method is to keep the quad tree in sync with the arrangement of GO’s as they change during the evaluation of the block argument. Refer to the discussion in the
GFGraphicObject section of
Basic Concepts and Classes.
Mementos
See the comments and code in GFMemento and GFBoundedStack for implementation. See the Drawing Editor and GFDrawingInterface for details of usage. For background on the memento approach, see Design Patterns by Gamma, Helm, Johnson, and Vlissides, an excellent reference for your library. Our Smalltalk implementation of the basic memento pattern discussed in the Gamma book extends it to make it more flexible and useful.
GFMemento is a generalized class for recording state information while trying to preserve encapsulation. It should only be created via one of the originator:state: methods, of which there are several variations.
A GFMemento preserves encapsulation by allowing access to its type and state information only if strict conditions are met. These conditions are always met if the originator of the memento is identically equal to (==) the requester object. The memento can also hold onto an accessKey. The accessKey is a symbol. When the requester object is not the same as the originator, the message specified by the accessKey is sent to the originator and the requester. If the results are equal (i.e., as determined by the = comparison), then the requester is allowed access to the memento’s type and state. A good example of an accessKey would be #class. If the accessKey is specified as #class, then any object of the same class as the originator can get access to the memento’s internals. If the accessKey is nil, then the requesting object must be == to the originator.
The default action is for a Symbol to be stored into the type field. The restoreUsing: method (implemented in Object) will perform the type symbol using the state information as the parameter.
Undo is enabled by default whenever you use a GFDrawingInterface. Of course, this means you have to provide a “hook” via a menu for your end-users to be able to take advantage of it. If you’re not going to let your users make use of the feature, you should definitely disable it, as it uses up unnecessary memory by holding onto GO’s and the state information needed to undo each operation. Disable it via:
interface noUndo.
The undo facility provided with GF/ST is not specific to GF/ST. You can use it in your own application. However, use of GFMemento requires design thought on your part. The type and state information of a memento must be supplied by your application - it does not come for free.
Printing
Printing is invoked via:
interface outputToPrinter.
When you print, only the region that encompasses the GO’s in your drawing is printed at the scale you specify. Multi-page printing is handled automatically. You can print in landscape mode by selecting it in the standard print dialog. Since printing performance and abilities are tied very closely to drivers and hardware, please consult the README file for the most current information we have concerning any limitations on printing. Always be sure to use the most up-to-date printer drivers. If printing works properly on one machine or on one printer in your application, but not on another, you should suspect machine and/or driver setup, not GF/ST or your application. If you have problems printing drawings that contain bitmaps (including GFGroupGO’s), you should try using:
aGO cacheFlag: false.
By setting the cacheFlag to false, GF/ST will not use a bitmap to cache the GO, and printing may be improved. In particular, problems may be cured for some PostScript printers that have difficulty printing opaque bitmaps.
Mostly Handle-Related How To’s
Handles are extremely powerful and thus extremely parameterized. This design approach makes it possible for you to use handles in very different ways without creating new kinds of handles to get different behaviors.
Track Handles
Track handles are used to translate mouse movement into actions on a GO. Track handles hold a change selector to specify the message which will be sent to the GO, along with a Point that typically specifies how much the mouse moves. Track handles come in several flavors
•GFTrackHandle - The simplest track handle. Holds onto a GO and a change selector. When you grab hold of the handle and move the mouse, it send the message specified in the change selector to the GO with the mouse movement specified as a Point.
•GFParameterizedTrackHandle - Allows you to pass another parameter with the mouse movement. With the parameterized track handle, you can take advantage of more elaborate protocol implemented on a GO and route the result of handle movement to any other object, including your own application.
•GFSelectionTrackHandle - Adds the ability to apply changes to all GO’s currently selected in the interface
•GFAbsoluteTrackHandle - Adds the ability to send absolute cursor positions instead of mouse movement deltas.
GFTrackHandle
The following code creates a handle on a GO, located at its center. The selector translateBy: is the change selector.
GFTrackHandle
on: aGO
at: #center
change: #translateBy:.
When the handle is moved it will perform the following:
aGO translateBy: delta.
To use a method like offCenter: on the GO, which requires a Point as an argument to specify how much “off center” the handle will be, there is more elaborate protocol on GFTrackHandle:
GFTrackHandle
on: aGO
at: #offCenter:
with: 5@5
change: #translateBy:.
GFParameterizedTrackHandle
GFParameterizedTrackHandles add the ability to incorporate a parameter with the change method. The following example assumes we are using a GFPolylineGO. To put a handle on the first point of the polyline, use:
GFParameterizedTrackHandle
on: aGO
at: #point:
with: 1
change: #movePoint:by:
with: 1.
Movement of the handle by some delta results in the following:
aGO movePoint: 1 by: delta.
An more elaborate very useful approach is to use a sense selector to compute a point from the mouse movement delta, and pass that result as an argument to the change selector. The protocol is:
GFParameterizedTrackHandle
on: aGO
at: #offCenter:
with: 5@5
sense: #senseBottomRight:
change: #growBy:.
When the mouse moves by some delta, the following expression is effectively evaluated:
aGO growBy: (aGO senseBottomRight: delta).
Routing Messages Directly to Other Objects
You do not always have to let the GO be the receiver of the change selector message. By using a Message object as the change selector instead of a symbol, you can specify some other receiver and selector to be used.
GFTrackHandle
on: aGO
at: #offCenter:
with: 5@5
change: (Message
receiver: myApp
selector: #rotateBy:).
In the above example, handle movement by some delta will result in the equivalent of:
myApp rotateBy: delta
Handles, Scrolling, and Messages
You may not want a drawing to scroll while a user drags a handle. In this case, simply send the handle the message:
handle scrollFlag: false.
You can tell a handle whether or not to stop invoking the change selector when the mouse moves outside of the bounds of the drawing using:
aHandle limit: aBoolean.
Handles and Grids
If the grid is being used, you can limit the deltas to increments of the grid size. By positioning a GO at a particular location that aligns with the grid, future handle movements will then always end up on grid points.
aHandle snapToGrid: aBoolean.
Taking Action When a Handle is Used
You can specify actions to be evaluated when you invoke a handle and when you release a handle. This mechanism is used in the Drawing Editor to update the status panes telling what a handle does, as well as clearing them when you let go.
One example of an action you could perform when the mouse button is released is shown below:
aHandle releaseAction: [Transcript show: ‘I let go’; cr].
To take some action when a handle is invoked, you can handle the event #handleInvoked, which is triggered by the GFDrawingInterface.
Setting the Handles for a GO
Whenever a GO needs handles (i.e., whenever it is selected), it triggers the event #generateHandles. If you do not handle the event, you will get the default set of handles for the GO. The default handles will work fine for initial development of an application, but they are almost guaranteed to be inappropriate for your end application. To specify the handles that are appropriate for your application’s GO’s, write a method that returns a collection of handles:
Simple Sizing Handles on Corners
| aGO myApp |
aGO := GFEllipseGO ellipse: (0@0 extent: 25@25).
myApp := MyApplication new.
“simple position tracking handles on the corners”
aGO
when: #generateHandles
send: #allCornersOf:
to: GFSelectionTrackHandle
with: aGO.
The result of the above code snippet is that, when you select the GO, the following message is evaluated:
GFSelectionTrackHandle allCornersOf: aGO.
and it returns a collection of handles at the corners of the GO.
Letting the Application Decide
Often, the state of an application determines what handles are appropriate. Also, since handles have so much behavior themselves, you may need to decide in your application logic what events handles will be handling, and what kinds of actions they will invoke. The typical way to deal with the problem is to write a handlesFor: method in your application. Then, when you create the GO, simply tell it:
aGO
when: #generateHandles
send: #handlesFor:
to: self
with: aGO.
Setting the Position Handle for a GO
A position handle is the handle that is created when a GO is to be moved. While position handles are primarily used to move a GO, you may find other uses for them in your application. For example, you may want to send some message to another object whenever you let go of a the GO you are positioning. In that case, you could create the GFPositionHandle in a positionHandleFor: method in your application, and specify the releaseAction for it when you create the handle:
aGO
when: #needsPositionHandle
send: #positionHandleFor:
to: self
with: aGO.
Event-Related How To’s
Events are central to GF/ST architecture, so you’ll be using them a lot whether you know it or not. Refer to
The GF/ST Event Model for more information on the basics of events. If you want to know when or why an event is triggered, refer to
Events Explained in the
Class Hierarchy, Protocol and Events section. In this section, we’ll try to anticipate some of the questions you might have about events when you start developing an application using GF/ST.
Tracing the Effects of an Event
We’re all used to looking at senders and implementors to track how messages flow through Smalltalk. When you use events, an event may be triggered in a method whose name is different than the event name, and it may result in execution of a method that is different than the event name. Consider the case when you use the #getMenu event to be able to provide your own menu to your drawing.
interface drawing
when: #getMenu
send: #networkMenu
to: self
If you look for implementors of getMenu, you will find that nobody implements it. When you look for senders of getMenu, you will find the methods that trigger the event. These methods are not sending the message getMenu, they are sending the message triggerEvent:, with #getMenu as the argument. Fortunately, “senders” locates all methods that use the symbol #getMenu.
If you look at senders of networkMenu , you’ll only find the method where you set up the dependency; i.e., the method that does the when:send:to:. Let’s take a look at the #getMenu event to see what’s really happening.
Examining senders of getMenu, we see the only interesting case is the menu method in GFGraphicObject:
menu
“Answer the menu of the receiver”
^self triggerEvent: #getMenu
ifNotHandled: [self standardMenu].
Now, if you put a breakpoint in your networkMenu method and popup a menu in the GFNetworkEditor example, this is the stack you’ll see:
Line | Context Stack |
1 | ’Breakpoint #1’ |
2 | DbgBreakpoint class>>#breakpointEncounterd: |
3 | GFNetworkEditor>>networkMenu |
4 | DirectedMessage >>send |
4a | DirectedMessage>>#perform |
4b | DirectedMessage>>#evaluate |
5 | GFDrawing(PsiEventModel)>>triggerEvent:ifNotHandled: |
6 | GFDrawing(GFGraphicObject)>>menu |
7 | GFDrawingInterface>>popupMenu |
8 | GFSelectionTool(GFTool)>>button2Down: |
9 | GFDrawingInterface>>button2Down: |
10 | GFDrawingPane>>button2Down: |
Looking at line 6, you can see that the menu method in GFDrawing (inherited from GFGraphicObject) ends up invoking the networkMenu method in GFNetworkEditor. It does this not by saying:
aGFNetworkEditor networkMenu
but by triggering the #getMenu event:
self
triggerEvent: #getMenu
ifNotHandled: [self standardMenu].
This means that if you look for senders of networkMenu, you will not find the menu method in GFGraphicObject. Instead, you must trace it via the when:send:to: and the method that triggers the event #getMenu.
In line 5, the triggerEvent:ifNotHandled: method, we determine that the instance of GFNetworkEditor is handling the event, and we evaluate the “Action” for it. This action was created when we originally sent the message when:send:to: to the instance of GFDrawing.
If everything goes well, you need only know how to use the event model. However, when things go awry, you may need to understand how it works in order to debug. If you don’t understand how the event model works, set up a hook to some event, put in a halt, and watch the code execution just as we did above.
Adding New Events
Remember from the discussion in
The GF/ST Event Model that there are two sides to events: first, registering a public interface as to what events may be triggered in a subclass of PsiEventModel, and second, triggering the event at the appropriate time.
To register a new event in a subclass of PsiEventModel, you must add it to the list which is returned from the class method eventsTriggered. For example, the eventsTriggered method in GFLineGO looks like this:
eventsTriggered
“Answer the events triggered”
eventsTriggered isNil ifTrue: [
(eventsTriggered := Set new)
addAll: self superclass eventsTriggered;
add: #moveStart;
add: #moveEnd.
].
^eventsTriggered
Note that the variable eventsTriggered is a class instance variable. Class instance variables are not class variables (which are visible to the class and all subclasses), they are instance variables for the class itself. Therefore, when we ask the class GFLineEndGO for its eventsTriggered, we will get a different set of events than we get for GFGraphicObject.
All eventsTriggered methods which we supply with GF/ST use a lazy initialization technique (so if eventsTriggered is nil, it initializes itself before returning). We also always inherit the events held in eventsTriggered in all superclasses.
When you want to add an event to the list in eventsTriggered in an existing class in GF/ST, you should first edit the class method eventsTriggered to add a new event symbol, and then reset the eventsTriggered instance variable for that class and all of its subclasses via:
TheClass resetEventsTriggered.
If you do not send the message resetEventsTriggered, neither the class nor any of its subclasses will return the new event in their list (since they use the class instance variable, not a class variable). As a matter of fact, it is safest simply to evaluate:
PsiEventModel resetEventsTriggered.
Once you add in the symbol to the eventsTriggered and you reset the class instance variable, you can use triggerEvent: in a method of the class, and you can set up notifications to dependents using the when:send:to: protocol. It is possible to trigger an event which nobody cares about, and it happens all the time. However, when you send the message when:send:to: to an instance of a PsiEventModel subclass, we check to see if the event is in the list of eventsTriggered for the class. If it is not, you will receive an error:
Bad event: theEventSymbol.
Fix the problem by adding the event in, as discussed about, or by fixing the argument in when:send:to: to use a supported event.