GFNetworkNodes will need the usual cast of accessing methods, at least if we want to provide public access to their state information. We also need to provide behavior for adding and removing connections between nodes, and for assigning and retrieving the weights. It is our decision that nodes will always know about all their connections; i.e., when I connect to you, I hold onto you in my collection of connections, and you will hold onto me in yours. The byproduct of this assumption is that nodes need to be smart about disconnecting themselves from other nodes. If you have an object that is holding onto a node (like a drawing, or a network), and you want to delete a node from it, you need to tell the node to disconnect itself from everything before removing it. Otherwise, you may be holding onto other nodes that hold onto the node you just deleted. Ain’t objects great? This little issue is one of the good reasons to use a GFNetwork object for all public protocol related to adding and removing nodes, rather than letting it be obfuscated in a GluePuppy subclass (even though we don’t do it in this prototype).
The initialize method is invoked automatically when new instances of ViewManager are created using the class method
new and when instances of ApplicationModel are created using the class method
open. The
initialize method is a good place to set up our drawing and its interface. At a minimum, it should look like this:
In lines 4-
6, we prompt for a name and create an instance of our domain model class, GFNetworkNode. GFNetworkNodes know nothing about how or if they are displayed. To do that job, we need a graphic object, which we create in the
nodeGOFor: method seen in line
7. We’ll be discussing it more later on. For now, it could just return a new instance of GFRoundedRectangleGO.
Note that line 4 is the only line that is Visual Smalltalk specific. There are equivalent expressions in VisualWorks and VisualAge for prompting a user.
Lines 8-
12 show a single
when:send:to:with: message that hooks into the
generateHandles event that every GO triggers when it needs to display handles. By telling the GO that we will handle this event, we are saying that
we will be the ones returning the handles for the
nodeGO in our
handlesFor: method. The
nodeGO that triggers the event will pass itself along as the argument, so we can ask it any questions we want. Similarly, in lines
13 and
14, we are telling the GO to let us know to update the weight display whenever it is selected or deselected. We only want weight to show up when a connection is selected, not a node, so we need to know when a node is selected to be able to clear the display.
In line 15, we set the origin of the GO to be at the center of the visible area of the drawing. The drawing itself is, by default, the same size as the display device you’re on, but you may have scrolled up or down. The interface takes care of tracking where the visible area is relative to the drawing as a whole, so you can use it instead of worrying about things like
topCorner as you would with a GraphPane in Visual Smalltalk.
Up to this point in the method, we have drawn nothing on the screen, we have only created the GO, set up the events, and set up where it will be drawn. In line 17, we actually draw the GO on the screen by telling the interface to
addGO:. As soon as it is on the screen, you get all the default behavior of the GO, so you can move it around and so on. However, because we hooked into the
generateHandles event, we must have our own
handlesFor: method. You can try it out without hooking into the
generateHandles event, and you will get the default handles for the kind of GO you returned from the
nodeGOFor: method.
Last, but not least, line 18 shows how we are keeping track of each
nodeGO we create in our own
nodeGOs instance variable. Sure, the interface holds onto these things, but we just thought it would be clearer to hold them directly. In effect, this collection of GO’s is our network, since each GO knows what actual instance of GFNetworkNode it models. We set up
that information in the
nodeGOFor: method, our next topic.
Refer to the “How To’s“ section for details of the protocol for creating various kinds of graphic objects. GF/ST provides many class-specific instantiation methods that should meet your needs.
Lines 4 and
5 set up the height and width for the GO based on the name of
aNode, the system font, and a factor of 3/2 times the string size. In line
6, we create a new instance of a GFTextGO with the node’s name in it. Once we have it, we want to push it to the right so it is centered. Be careful with point arithmetic to use Visual Smalltalk methods like
rightAndDown:. In lines
8 and
9, we create a new, yellow GFRoundedRectangleGO. Then, in line
10, we create a GFGroupGO and add the text and rounded rectangle to it. None of these operations has caused any display activity, which is handled via the interface in the
addNode method of GFNetworkEditor.
If you want a GO to hold onto another object, and in particular, if you want it to know about its domain model, you can use the metaObject for it. As in classic MVC-style programming, you should consider it to be perfectly proper for a graphic object to know about its underlying domain model, but you should avoid
any knowledge in your domain model classes of how they are displayed. A side note for you MVC haters out there: loose coupling is the
good part of MVC, duplication of view/controller protocols and rampant updates is the
bad part. It’s not all bad! A side note for those of you who don’t know what MVC is: count your blessings. Just remember to factor your domain model classes out of your application, and never let them know how they are being displayed.
Line 11 shows how to set the
metaObject for our new GFGroupGO we are using to display
aNode. Similarly, if you have the GO and you want to find out what its node really is, just ask it:
Now, we need to implement a handlesFor: method. There are a number of special things we want to accomplish in this method which we will be discussing in detail. Overall, however, the purpose of this method is to return the proper set of handles for the GO, with the proper positioning and behavior. This is our opportunity to override the default handle behavior for a GO. Here is what it looks like:
When we created the GO, we set its metaObject to the specific GFNetworkNode it was displaying. Now we are going to use it, so we hold onto the
node in a temporary variable in line
4.
In lines 5-8, we create an instance of a GFConnectionHandle using the
on:at:with: class-specific instantiation message. The arguments are the GO, a message selector that will return the appropriate point,
#offCenter:, and the argument for that selector. We use our own
connectionLocationFor: method simply to return a point that is 3/8 of the GO’s bounding box height.
In line 9-10, we specify an action of some kind (i.e., a block or an EvaluableAction - anything that responds to the protocol of EvaluableAction) which, when evaluated, returns a GFLocator on a point. That point will be the place that lines snap to as you drag the cursor around while connecting nodes in the drawing. Note that the arguments which are passed to the
locatorGenerationAction are the GO being connected to, and the mouse location.
The locatorFor: method referenced in line
9 says:
Refer to “GFLocator“ for a discussion of locators. Typically, the connection handle location, the point returned by the
locatorGenerationAction, and the point returned by the
targetPositionAction (discussed in “
Where Are Connections Drawn To?“) will all return the same point, but we didn’t want to limit your options.
Once again, the argument to the setFindTargetAction: method is an action of some kind (in this case a block) that is evaluated as you drag the mouse over GO’s in the drawing. If the result of evaluating the action is a GO, GF/ST will evaluate the
locatorGenerationAction to determine where to connect to. If the result is
nil, you will not be able to connect to that GO. The arguments supplied by GF/ST are the GFDrawingInterface and the location of the mouse. The logic in lines
12-17 asks the interface to determine what GO is located at the point, and it only returns the GO if it is not a “connection” GO and if the node itself is not already connected to the target node.
In line 18, we set the
targetPositionAction for the handle. Typically, the target position is the same as the location as is specified by the locator used in the
locatorGenerationAction, but it does not have to be. You would make the
targetPositionAction return a different point if you want the connecting lines to connect to a different point than they snap to during the connection process.
The targetPositionAction, when evaluated with the
targetGO as the argument, must return a Point. Note that the
locatorGenerationAction returns a GFLocator.
When we connect one GO representing a node to another, we need to change the state of the underlying domain model nodes, as shown in line 20. While we’re at, if we need to take any action when the user clicks on the connecting lines themselves, we might as well set up the events at the time the connection is made. We want update the display of connection “weight” in the GFNetworkEditor whenever we select or deselect a connecting line, as shown in lines
21-22. It makes the display look nicer if we always push the connecting line to the back, behind all other GO’s, and this is done in line
23. Finally, users felt it made connecting nodes easier if we select the one we just connected to each time, as shown in line
24.
It is simple to remove GO’s from a drawing using the removeGO: method of the GFDrawingInterface. However, our job is more complicated in the Network Editor. We are not just removing it from the display, but we need to unhook the connections in the display and the underlying domain model.
Referring to the deleteNode method in the GFNetworkEditor, you can see how the GO’s are used to determine what domain model objects are affected by a deletion of multiple nodes on the screen.
In line 4, we loop over all the GO’s which are selected in the drawing. The interface returns a collection of the selected GO’s when we send it the message
selections. We need to make a
copy of that collection because we will be removing GO’s as we iterate over it. If we don’t make a copy, bad and mysterious things will happen in Visual Smalltalk. As a general rule,
never modify a collection as you iterate over it - make a copy of it, or find some other way to accomplish your objective.
In line 5, we filter-out all of the connection GO’s; i.e., we are writing a method to remove nodes, not connections. When we remove a node, we will take care of any affected connections.
In line 7, we remove the node GO from the interface. We are tracking these GO’s in our instance variable,
nodeGOs, so we need to remove it from there, too, as shown in line
8. We decided when we built the domain model that GFNetworkNodes should know how to disconnect themselves from the nodes they connect to, and this is accomplished in line
9. Once again, we can send a message directly to the GFNetworkNode using the
metaObject of the GO.