Adding Mouse Tracking to GraphPaper

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!

Changes to the GraphPaper Interface

  1. 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).

  2. Subclass the ColorGraphView class and rename the new subclass “TrackingGraphView”.

  3. Add outlets called xCell and yCell to the TrackingGraphView class.

  4. Change the class of the ColorGraphView instance in the GraphPaper window to TrackingGraphView in the Class Info dialog.

  5. Verify that the Graph button is still connected to the TrackingGraphView instance. Make sure it sends the graph: action.

  6. 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.

New x: and y: text fields to identify points on a graph in GraphPaper

Figure 18-1. New x: and y: text fields to identify points on a graph in GraphPaper

  1. 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.

  2. 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.

  3. Connect the TrackingGraphView’s xCell outlet to the NSTextField labeled “x:” and the yCell outlet to the one labeled “y:”.

  4. Create the TrackingGraphView class files and insert them into your project.

Changes to the TrackingGraphView Class Files

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.

  1. 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.

  1. Insert the following #import directive into TrackingGraphView.m:

                            #import "Segment.h"
  2. 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.

  1. 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.

  1. 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.

  1. 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];
                            }
  2. 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.

  1. Build and run GraphPaper. Save all files first.

  2. 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.

GraphPaper window with crosshair at cursor hot spot and with x and y values on graph

Figure 18-2. GraphPaper window with crosshair at cursor hot spot and with x and y values on graph

  1. Quit GraphPaper.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.165.70