Now we know enough to add mouse tracking to GraphPaper. The modifications will involve the following four parts:
Modifying the initWithFrame: method to set up the tracking rectangle around the on-screen ColorGraphView instance
Adding a mouseEntered: method that will tell the NSWindow to start sending mouse-moved events
Adding a mouseMoved: method to process mouse-moved events
Adding a mouseExited: method that will reset the window’s event-handling status
Rather than adding this new functionality to the GraphView or ColorGraphView classes, we’ll subclass ColorGraphView to make a TrackingGraphView class. Tracking functionality is separate from graphing functionality, and it makes sense to separate them in the code.
Let’s get on with it!
Back in IB, make sure that MainMenu.nib
is open
and is the selected window (again, we recommend that you minimize the
other nibs that are open in IB).
Subclass the ColorGraphView class and rename the new subclass “TrackingGraphView”.
Add outlets called xCell
and
yCell
to the TrackingGraphView class.
Change the class of the ColorGraphView instance in the GraphPaper window to TrackingGraphView in the Class Info dialog.
Verify that the Graph button is still connected to the TrackingGraphView instance. Make sure it sends the graph: action.
Add two NSTextField objects inside the GraphPaper window and label them “x:” and “y:”, as shown in Figure 18-1. It may be necessary to resize the GraphPaper window to accommodate the two text fields.
Make the two new NSTextFields uneditable but selectable in the Attributes Info dialog. Set their borders to be solid lines, as shown in Figure 18-1.
Using the Size inspector, set the “springs” for both NSTextFields in the same way you did for the Graph button: the topmost and leftmost lines should be springs, so that the text fields do not resize but instead move with the lower-right corner.
Connect the TrackingGraphView’s
xCell
outlet to the NSTextField labeled
“x:” and the
yCell
outlet to the one labeled
“y:”.
Create the TrackingGraphView class files and insert them into your project.
In addition to the two outlets
we set up in IB, the TrackingGraphView class needs two more instance
variables. The trackingRect
instance variable will
remember the NSTrackingRectTag that is returned when the tracking
rectangle is created. (We don’t use the variable in
this class, but it is conceivable that a subclass might use it.) We
will also create a second NSMutableArray, called
annotations
, that we’ll use to
keep track of the additional annotations (i.e., two lines that make
up a big crosshair) that we will display on the TrackingGraphView.
Insert the lines shown here in bold into
TrackingGraphView.h
:
#import <Cocoa/Cocoa.h>#import "ColorGraphView.h" @interface TrackingGraphView : ColorGraphView { IBOutlet id xCell; IBOutlet id yCell; NSTrackingRectTag trackingRect; NSMutableArray *annotations; } - (id)initWithFrame:(NSRect)frame; - (void)mouseEntered:(NSEvent *)theEvent; - (void)mouseExited:(NSEvent *)theEvent; - (void)mouseMoved:(NSEvent *)theEvent; - (void)addAnnotation:(id)anObject; - (void)removeAnnotations; @end
We’ll set up seven methods in the TrackingGraphView class to make it draw a big crosshair over the entire view when the mouse cursor moves over the graph (the mouse cursor itself will remain a pointer). The first method, initWithFrame: , will establish a tracking rectangle around the TrackingGraphView.
Insert the following #import
directive into
TrackingGraphView.m
:
#import "Segment.h"
Insert the following initWithFrame:
method implementation (which overrides the one in ColorGraphView)
into TrackingGraphView.m
:
- (id)initWithFrame:(NSRect)frame { [super initWithFrame:frame]; annotations = [ [NSMutableArray alloc] init]; trackingRect = [self addTrackingRect:[self visibleRect] owner:self userData:nil assumeInside:NO]; return self; }
The initWithFrame: method is the NSView designated initializer. This method calls the designated initializer in the superclass, then adds an NSMutableArray to keep track of the annotations. Finally, a tracking rectangle is created for the portion of the TrackingGraphView that is currently visible on the screen.
If the user resizes GraphPaper’s main window, the tracking rectangle will no longer be correct. The override of the getFormAndScaleView method shown in the next step resizes the tracking rectangle whenever the TrackingGraphView is resized.
Insert the following getFormAndScaleView method implementation into
TrackingGraphView.m
:
- (void)getFormAndScaleView { [self removeTrackingRect:trackingRect]; // Remove the old [super getFormAndScaleView]; trackingRect = [self addTrackingRect:[self visibleRect] owner:self userData:nil assumeInside:NO]; }
The next pair of methods responds to events generated by the cursor’s entering and exiting the tracking rectangle.
Insert the following
mouseEntered:
and mouseExited: method
implementations into TrackingGraphView.m
:
- (void)mouseEntered:(NSEvent *)theEvent { [ [self window] setAcceptsMouseMovedEvents:YES]; [ [self window] makeFirstResponder:self]; } - (void)mouseExited:(NSEvent *)theEvent { [self removeAnnotations]; [ [self window] setAcceptsMouseMovedEvents:NO]; }
The mouseEntered: method makes the TrackingGraphView the NSWindow’s first responder and changes the NSWindow’s event mask so that it gets all mouse-moved events. ThemouseExited: method restores the original event mask. (We’ll discuss the removeAnnotations method shortly.)
The next two methods maintain the annotation list. The crosshair that TrackingGraphView draws will be added to two display lists: the first is the normal display list maintained by the GraphView; the second is the list of annotations. This second list allows us to remove the annotations from the primary display list without recomputing the entire graph.
Insert the following addAnnotation:
and removeAnnotations methods into
TrackingGraphView.m
:
- (void)addAnnotation:(id)obj { [annotations addObject:obj]; [self addGraphElement:obj]; } - (void)removeAnnotations { NSEnumerator *en = [annotations objectEnumerator]; id obj; while (obj = [en nextObject]) { [self setNeedsDisplayInRect:[obj bounds] ]; } [displayList removeObjectsInArray:annotations]; [annotations removeAllObjects]; }
Insert the following mouseMoved:
method into TrackingGraphView.m
:
- (void)mouseMoved:(NSEvent *)theEvent { NSPoint pt; NSEnumerator *en; id obj; pt = [self convertPoint:[theEvent locationInWindow] fromView:nil]; en = [displayList objectEnumerator]; while (obj = [en nextObject]) { if ([obj tag]==GRAPH_TAG && pt.x >= [obj bounds].origin.x && pt.x <= [obj bounds].origin.x + [obj bounds].size.width) { // Are we within 30 pixels of the line in screen coordinates? NSPoint ptMouse = [theEvent locationInWindow]; NSPoint ptLine = [self convertPoint:[obj segmentCenter] toView:nil]; double dist = sqrt(pow(ptMouse.x - ptLine.x,2) + pow(ptMouse.y - ptLine.y,2)); if (dist<30.0) { // Add two segments to annotations NSRect vb = [self bounds]; NSRect ob = [obj bounds]; id seg; [self removeAnnotations]; // Remove the old // Horizontal line intersecting cursor hot spot seg = [ [Segment alloc] initFrom:NSMakePoint(vb.origin.x,ob.origin.y) to:NSMakePoint(vb.origin.x+vb.size.width, ob.origin.y)]; [seg autorelease]; [self addAnnotation:seg]; [seg setColor:[NSColor greenColor] ]; // Vertical line intersecting cursor hot spot seg = [ [Segment alloc] initFrom:NSMakePoint(ob.origin.x,vb.origin.y) to:NSMakePoint(ob.origin.x, vb.origin.y+vb.size.height)]; [seg autorelease]; [self addAnnotation:seg]; [seg setColor:[NSColor greenColor]]; // Update positions in the x and y text fields [xCell setStringValue: [NSString stringWithFormat:@"x: %g", [obj segmentCenter].x] ]; [yCell setStringValue: [NSString stringWithFormat:@"y: %g", [obj segmentCenter].y] ]; [self setNeedsDisplay:YES]; return; } } } // No segment should be highlighted [self removeAnnotations]; [self display]; [xCell setStringValue:@"x:"]; [yCell setStringValue:@"y:"]; }
Despite the length of this mouseMoved:
method, it isn’t very complicated. First
it converts the new mouse location from NSWindow to NSView
coordinates. Then it iterates through the display list, searching for
a segment that has the GRAPH_TAG
tag and also
contains the point corresponding to the mouse position. If it finds
such a segment, and if the mouse is within 30 pixels of the
segment’s center, it removes the old annotation
(crosshair) lines and adds two new ones — a horizontal line and a
vertical one. The x: and y: text fields are then filled in, and the
entire view is redisplayed. (Ideally, you should be able to do a
redisplay of only the region that has been updated, but that code
didn’t work for us, possibly due to a Cocoa 10.1
bug.) If the mouse position doesn’t correspond to
any Segment, the annotations are removed and the x: and y: values are
erased.
Build and run GraphPaper. Save all files first.
Click the Graph button and move the cursor over the graph.
The window in Figure 18-2 shows what the x: and y: cells and highlighted Segment look like when GraphPaper runs. Note that the arrow cursor is at the center of the big crosshair.
18.226.165.70