Building the GraphPaper Application

Now that we’ve thought about GraphPaper a bit, let’s get on with the work of building the application.

Changes to the Evaluator Back End

We need to make one change to Evaluator so that it can recognize more than a single expression on a line — that’s important, because GraphPaper will be sending (x,y) pairs such as (3,2*3+1) to Evaluator. (If we sent only y values, we might get confused.)

The easy way to do this is to make Evaluator recognize two expressions separated by a comma and terminated with a newline. Because we built Evaluator with lex and yacc, this change is easy to make and is confined to a single file, grammar.y.

  1. Launch Project Builder, choose File New Project, and double-click “Cocoa Application”.

  2. Type “GraphPaper” in the Project Name field, hit the Tab key so that the project is saved in the folder ~/GraphPaper, and then click Finish.

  3. Create a second target called Evaluator in the GraphPaper project by choosing PB’s Project New Target menu command, double-clicking Tool at the bottom of the list in the resulting sheet, typing “Evaluator”, and clicking Finish in the second sheet in the project window.

  4. Click the Files vertical tab in PB’s main window and disclose the Resources group.

  5. Activate the Finder, open your ~/MathPaper folder, single-click grammar.y, and then Command-click rules.l to select these two files. We’ll add them to the GraphPaper project in the next step.

  6. Drag the two-file selection from the Finder and drop it on the Resources group in PB’s Groups & Files pane. In the resulting sheet, click the checkbox next to “Copy items into destination group’s folder”, make sure the Evaluator target is checked, and finally click Add.

  7. In PB’s editor, insert the six lines shown here in bold into grammar.y:

    stat  : expr '
    '
    {
      printf("%10g
    ", $1);
      printingError = 0;
      fflush(stdout);
    }| expr ',' expr '
    '
                            {
                              printf("%g,%g
    ", $1, $3);
                              printingError = 0;
                              fflush(stdout);
                            }
    ;

These changes enable us to send to Evaluator two expressions on the same line, separated by a comma. Evaluator will evaluate each expression and print the results, separated by a comma, on a single line.

  1. Select Evaluator in the pop-up menu at the top center of PB’s main window, then click the build and run button to build the Evaluator target. Save grammar.y before building.

  2. Test the new running Evaluator process in PB’s Run pane by typing the expressions shown here in bold. The nonbold lines are output from Evaluator.

                            0,2*0+1
    0,11,2*1+1
    1,32,2*2+1
    2,5

In this example, we typed the seven-character string “0,2*0+1” and Evaluator responded with “0,1”. Note that the upgraded Evaluator can now handle the numeric algebraic pairs that we plan to send it later.

  1. Quit Evaluator by clicking the Stop button in PB’s toolbar.

  2. Still in PB, add the Evaluator file to the GraphPaper target. To do this, select the GraphPaper target in PB’s pop-up menu, choose Project Add Files, and add Evaluator (in the build folder) to the GraphPaper target.

Building GraphPaper’s Interface

In this section, we will put in place the underlying framework for the GraphPaper application:

  1. Using a graphics program, create a 128 x 128 bit application icon for GraphPaper. Our amateurish attempt is shown here.

    Building GraphPaper’s Interface

    You might also create the 48 x 48, 32 x 32, and 16 x 16 icon sizes to use in GraphPaper’s .icns file.

    Building GraphPaper’s Interface

  2. Launch IconComposer, create a GraphPaper.icns icons file using the icon(s) that you created in the previous step, and save this file in the ~/GraphPaper project folder.

  3. Drag the GraphPaper.icns icon from the Finder and drop it in the Resources section of PB’s Groups & Files pane. Add the GraphPaper.icns file to your GraphPaper target.

  4. Click PB’s Targets vertical tab and select the GraphPaper target. Click the Application Settings tab and establish the application settings, as specified in Table 16-2.

Table 16-2. Application settings for GraphPaper

Setting name

Value

Executable

GraphPaper

Identifier

GraphPaper

Type

AAPL

Signature

GRFP

Version

1.0

Display Name

GraphPaper graphical calculator

Get-Info String

GraphPaper

Short version

GraphPaper

Icon file

GraphPaper

Principal class

NSApplication

Main nib file

MainMenu

  1. Single-click the InfoPlist.strings filename (under Resources) in PB’s Groups & Files pane. Edit the copyright messages in PB as appropriate.

  2. Double-click the MainMenu.nib filename (under Resources) in PB’s Groups & Files pane. IB will automatically launch and display the default MainMenu.nib interface created by PB.

  3. Modify the GraphPaper menus, changing “NewApplication” to “GraphPaper” in four places in the application menu. Also, rename “MyApp” in the Help menu.

Earlier, we mentioned that a class called GraphView will be used to display function graphs. As you might expect, GraphView will be a subclass of Cocoa’s NSView class. GraphView will need outlets to point to most of the on-screen objects, as well as action methods to start and stop the graphing.

  1. Select NSView in the MainMenu.nib window and choose Classes Subclass NSView to create a new subclass. Rename the new subclass “GraphView”.

  2. Type Command-1 to open the GraphView Class Info dialog. Add the following outlets and action methods to GraphView:

                            Outlets                   Action Methods
    graphButton               graph:
    xminCell                  stopGraph:
    xmaxCell
    xstepCell
    yminCell
    ymaxCell
    formulaField
  3. Choose Classes Create Files for GraphView to create the files for the GraphView class and insert them into the GraphPaper target.

Next we’ll set up GraphPaper’s main window, as shown in Figure 16-2.

GraphPaper’s main window in IB

Figure 16-2. GraphPaper’s main window in IB

  1. Still in IB, change the main window’s title from “Window” to “GraphPaper” in the Window Info dialog.

  2. Resize the window so that it is about two inches tall and four inches wide.

  3. Drag a CustomView icon from IB’s Cocoa-Views palette and drop it in the GraphPaper window. Enlarge it and position it as shown in Figure 16-2.

  4. Change the class of the CustomView to GraphView in the Info dialog.

  5. Drag an NSForm object from IB’s Cocoa-Views palette and drop it in the right side of the GraphPaper window.

  6. Option-drag the bottom-center handle of the NSForm object to create three more NSFormCells, for a total of five NSFormCells.

  7. Change the labels on the NSFormCells to “xmin”, “xmax”, “xstep”, “ymin”, and “ymax”, as shown in Figure 16-2. (Use the Tab key to move quickly from one NSFormCell to the next.)

  8. Enter the numbers “0.0”, “10.0”, “0.1”, “-1.0”, and “1.0” in the five text areas of the NSForm, as shown in Figure 16-2. We’ll use these initial graphing parameters to show the user a good-looking graph at launch time.

  9. Select the entire NSForm matrix and change the text to be right-aligned in the Text Alignment box in the NSForm Info dialog.

  10. Drag a SystemFont Text icon from IB’s Cocoa-Views palette and drop it in the lower-left corner of the GraphPaper window. Change the text to “y(x)=”, as shown in Figure 16-2, and make it larger (16 point) using IB’s Font dialog (Command-T). This SystemFont Text icon represents an NSTextField object with attributes such as uneditable, etc.

  11. Drag an NSTextField icon from IB’s Cocoa-Views palette and drop it at the right of the “y(x)=” in the GraphPaper window.

  12. Make the text in the NSTextField larger (16 point) using IB’s Font dialog. Make the NSTextField wider as well.

  13. Enter a function that has an interesting graph in the white NSTextField. We’ll use sin(3*x), which will produce an interesting graph at launch time.

  14. Drag an NSButton object from IB’s Cocoa-Views palette and drop it in the GraphPaper window below the NSForm, as shown in Figure 16-2. Change the title on the NSButton object to “Graph” and make the text size 16 point. Use the blue guidelines to align the on-screen objects.

  15. Connect the seven GraphView outlets to the appropriate on-screen objects. For example, Control-drag from the GraphView on-screen instance to the Graph button and double-click the graphButton outlet. Similarly, connect the xmaxCell outlet to the NSFormCell labeled “xmax”, the xminCell outlet to the NSFormCell labeled “xmin”, and so on. The formulaField outlet should be connected to the NSTextField object containing the formula (see the GraphView Connections Info dialog at the left of Figure 16-3). Note that all of the outlet connections are listed at the bottom of the Info dialog.

GraphView’s outlet connections (left) and Graph button action connection (right)

Figure 16-3. GraphView’s outlet connections (left) and Graph button action connection (right)

Later in this chapter, we’ll use the graphButton outlet to temporarily change the title on the button from “Graph” to “Stop” while GraphPaper is drawing a graph.

  1. Connect the Graph button to the on-screen GraphView instance. Make it send the graph: action message to GraphView. See the corresponding NSButton Info dialog in Figure 16-3.

We won’t use the stopGraph: action as part of the connections we set up in IB, but we will connect the Graph button to stopGraph: programmatically. One tiny advantage of adding stopGraph: to the GraphView class in IB is that IB’s Create Files command will create the skeleton code for the method, which saves a bit of typing. Another advantage is that stopGraph: will be visible while you’re working in IB.

  1. Save the MainMenu.nib file.

The GraphView Class Interface File

GraphView will be the most complicated class that we build in this book, so we’ll go over it in pieces. When learning any new class, the best place to start is with the interface file. In this case, that file is GraphView.h.

  1. Back in PB, insert the lines shown here in bold into GraphView.h:

    #import <Cocoa/Cocoa.h>
    
    @interface GraphView : NSView
    {
        IBOutlet id formulaField;
        IBOutlet id graphButton;
        IBOutlet id xmaxCell;
        IBOutlet id xminCell;
        IBOutlet id xstepCell;
        IBOutlet id ymaxCell;
        IBOutlet id yminCell;
        // These five variables are the same as those in MathPaper
                                NSPipe       *toPipe;
                                NSPipe       *fromPipe;
                                NSFileHandle *toEvaluator;
                                NSFileHandle *fromEvaluator;
                                NSTask       *evaluator;
                            
        NSMutableString     *fromBuf;
                            
        // These hold the contents of the NSForm
                                // double      xmin;   These three will be public variables
                                // double      xmax;   See the @public directive below
                                // double      xstep;
                                double      ymin;
                                double      ymax;
                            
        // Display list
                                NSMutableArray *displayList;
                                BOOL            first;        // Getting the first point?
                                NSPoint         lastPt;       // Last point received
                            
        // Communication with stuffer thread
                                BOOL       stop_sending;
                                BOOL       sending;
                                BOOL       receiving;
                            
    @public    // For use by stuffer thread
                            
      BOOL      graphing;
                              char      *formula;
                              int       toFd;
                              double    xmin, xmax, xstep;
    }
    
    - (IBAction)graph:(id)sender;
    - (IBAction)stopGraph:(id)sender;- (void)doStop:(int)which;
                            - (void)getFormAndScaleView;
                            - (void)addGraphElement:(id)element;
                            - (void)clear;
                            - (void)sendData;
    @end
    #define STOP_SENDER 1
                            #define STOP_RECEIVER 2
                            
    #define GRAPH_TAG 1
                            #define AXES_TAG  2
                            #define LABEL_TAG 3

The first seven id statements declare the outlets we set up and connected in IB. The remaining instance variables are a little more complicated. Here is a brief description of what they do:

toPipe, fromPipe, toEvaluator, fromEvaluator, evaluator

These variables all have the same functions as the corresponding variables in the MathPaper application. The MathPaper variables were initially defined in Chapter 11.

fromBuf

It’s possible to get a variable amount of information back from Evaluator (including a partial line), so it’s necessary to buffer Evaluator’s content. We’ll use fromBuf, an NSString instance variable, as the buffer.

The next group of instance variables holds a copy of the graphing parameters that are read from the NSForm. We use instance variables to store the graphing parameters so that they can be referenced by both the main thread and the stuffer thread.

xmin, xmax

These two variables determine the horizontal scale of the graph that is drawn.

xstep

This variable determines the step increment in the horizontal (x) direction, used for drawing the graph.

ymin, ymax

These two variables determine the vertical scale of the graph that is drawn.

The next group of variables is used for holding and maintaining the display list:

displayList

This is the actual display list itself, implemented with an NSMutableArray.

first

This boolean variable is set before the first pair is received from Evaluator. It enables the GraphView object to distinguish between the first pair of coordinates returned and the others.

lastPt

The (x,y) coordinate pair of the last point read from Evaluator. This variable is valid only if first=NO. It is used to construct the line segment from the last point received to the current point.

The last group of instance variables is used for communication between the main thread and the stuffer thread. Because of the design of the GraphView class, it won’t be necessary to use an NSLock.

stop_sending

When set to YES, this boolean variable forces the stuffer thread to exit.

sending

This boolean variable is set to YES just before the stuffer thread starts up. When the stuffer thread is finished, it will send the termination code (999) to Evaluator and resets this variable to NO.

receiving

This boolean variable is set to YES just before the stuffer thread starts up. When the main thread receives the termination code (999) from Evaluator, it resets this variable to NO.

The @public declarations mean that the four instance variables (graphing, formula, etc.) will be visible everywhere, including to the stuffer thread. We’ll discuss the new methods declared in GraphView.h as we progress through this chapter. Finally, the #define statements set up the tags that we will use for various parts of the GraphView class.

The GraphView Class Implementation File

Now let’s look at the GraphView class implementation code in GraphView.m. The first part of the file requires another #import directive:

  1. Insert the #import directive for the Segment.h file near the beginning of GraphView.m:

    #import "GraphView.h"
    #import "Segment.h"
    
    @implementation GraphView

The new Segment class will be used to create line segments to draw pieces of the graph. We’ll create the Segment class later in this chapter.

The initWithFrame: Method

The first method we’ll discuss in our GraphView class definition is initWithFrame: , the view’s designated initializer. This method will set up the connection to Evaluator and will initialize the displayList and fromBuf instance variables.

  1. Insert the following initWithFrame: method into GraphView.m:

                            - initWithFrame:(NSRect)frame
                            {
                                NSString *path;
                                [super initWithFrame:frame];
                            
        displayList = [ [NSMutableArray alloc] init];
                                fromBuf = [ [NSMutableString alloc] init];
                            
        // What follows is largely from MathPaper
                                path  = [ [NSBundle mainBundle]
                                           pathForResource:@"Evaluator" ofType:@""];
                            
        if (!path) {
                                    NSLog(@"%@: Cannot find Evaluator", [self description]);
                                }
                                else {
                                    toPipe   = [NSPipe pipe];
                                    fromPipe = [NSPipe pipe];
                            
            toEvaluator   = [toPipe fileHandleForWriting];
                                    fromEvaluator = [fromPipe fileHandleForReading];
                                    evaluator = [ [NSTask alloc] init] retain;
                                    [evaluator setLaunchPath:path];
                            
            [evaluator setStandardOutput:fromPipe];
                                    [evaluator setStandardInput:toPipe];
                                    [evaluator launch];
                            
            [ [NSNotificationCenter defaultCenter] 
                                        addObserver:self
                                           selector:@selector(gotData:)
                                               name:NSFileHandleReadCompletionNotification
                                             object:fromEvaluator ];
                            
            [fromEvaluator readInBackgroundAndNotify];
                                }
                            
        // The notification below causes the getFormAndScaleView
                                // method to be invoked whenever this view is resized
                                [ [NSNotificationCenter defaultCenter] 
                                                addObserver:self
                                                   selector:@selector(getFormAndScaleView)
                                                       name:NSViewFrameDidChangeNotification
                                                     object:self];
                                return self;
                            }

The initWithFrame: method starts by creating the displayList and fromBuf objects. Then it creates Evaluator, using code that is largely borrowed from MathPaper (see Chapter 11). Finally, it makes GraphView a receiver of notifications of the NSViewFrameDidChangeNotification type. This notification ensures that the GraphView will be sent a getFormAndScaleView message if its on-screen view area changes size. By doing this, we avoid having to make GraphView a delegate of the NSWindow in which it resides.

Implementing the Display List

Recall that the data stuffer thread sends to Evaluator a series of expressions that looks like this (for y=2*x+1):

0, 2*0+1
1, 2*1+1
2, 2*2+1

And Evaluator sends back a series of numbers that looks like this:

0, 1
1, 3
2, 5

The GraphView object uses those pairs of numbers to construct a graph. For this to happen, we must create a display list — a list of objects that will be used to describe the drawing of a graph.

Our display list will be implemented with a series of objects that adopt a new formal protocol that we’ll call GraphViewElement. We say that an Objective-C class adopts a protocol if it implements all the methods in that protocol. A formal protocol is a group of methods declared between the @protocol and @end directives. A formal protocol is adopted in code by listing its name between angle brackets in a class declaration, as we’ll see later.

The methods in our GraphViewElement protocol are described in Table 16-3.

Table 16-3. GraphViewElement protocol methods

Method

Purpose

- (int)tag

Returns the object’s tag (used later)

- (void)setTag:(int)aTag

Sets the object’s tag

- (void)stroke

Draws the object

- (NSRect)bounds

Returns the element’s bounding box

- (void)setColor:(NSColor *)aColor

Sets the element’s color

- (NSColor *)color

Returns the object’s color

The GraphView class will maintain a list of objects that respond to this protocol in the displayList mutable array. The following GraphView methods will be used to implement this display list functionality:

- (void)clear

Empties the display list

- (void)addGraphElement:(id)element

Adds an element to the display list

- (void)drawRect:(NSRect)aRect

Draws the portion of the GraphView (and the GraphView display list) that appears within aRect

  1. Insert the GraphViewElement protocol into the GraphView.h file, after the @end directive that ends the GraphView interface:

                            @protocol GraphViewElement
                            - (int)tag;
                            - (void)setTag:(int)aTag;
                            - (void)stroke;
                            - (NSRect)bounds;
                            - (void)setColor:(NSColor *)aColor;
                            - (NSColor *)color;
                            @end

    Placing this protocol definition in the file GraphView.h informs the GraphView class about the declarations for each of these methods.

  2. Insert the following clear method into the GraphView.m file, after the @implementation directive but before the @end directive:

                            // Display list maintenance
                            - (void)clear
                            {
                                [displayList removeAllObjects];
                                [self setNeedsDisplay:YES];
                            }

    This clear method removes all of the objects from the displayList and then sends a message to itself indicating that the entire GraphView needs to be redisplayed.

  3. Insert the following addGraphElement: method into GraphView.m:

                            - (void)addGraphElement:(id)element
                            {
                                [displayList addObject:element];
                                [self setNeedsDisplayInRect:[element bounds]];
                            }

    This method adds the element object argument to the display list, then invokes NSView’s setNeedsDisplayInRect: method to tell the GraphView’s superclass that the region within the bounding box of the added element needs to be redrawn.

Scaling the GraphView and the drawRect: Method

As with PolygonView in the previous chapter, GraphView will use Quartz and the NSView architecture to provide all of the scaling that we need to draw our mathematical functions.

The only thing that our program needs to do is provide information for the required scaling. This will be done by the method getFormAndScaleView, which will read the current parameters from the on-screen window’s form, set up the GraphView’s instance variables, and then scale the GraphView’s bounds to the appropriate size.

  1. Insert the following getFormAndScaleView method into GraphView.m:

                            - (void)getFormAndScaleView
                            {
                                xmin  = [xminCell doubleValue];
                                xmax  = [xmaxCell doubleValue];
                                xstep = [xstepCell doubleValue];
                                ymin  = [yminCell doubleValue];
                                ymax  = [ymaxCell doubleValue];
                                [self setBounds:(NSMakeRect(xmin, ymin, xmax-xmin, ymax-ymin) ) ];
                                [self setNeedsDisplay:YES];
                            }

You might think that the drawRect: method, which we show in the next step, would be the workhorse of the GraphView class. After all, this method does all of the work of actually drawing the graph, right? But in fact, this method is very simple in GraphView. First it initializes the background color to white, then it determines an appropriate line width for drawing the graph and sets the current line width accordingly. (The default line width is 1 point, but because we will be rescaling the coordinate system of this NSView to match that of our graph, we need to calculate the “true” size of 1 point in our scaled coordinate system.) Finally, the drawRect: method iterates through all of the objects in the display list, determines whether or not they intersect the area that is being redrawn, and draws them if they do.

  1. Insert the following isOpaque and drawRect: methods into GraphView.m:

                            -(BOOL)isOpaque { return YES; }   // Because GraphView is opaque
                            
    -(void)drawRect:(NSRect )rect
                            {
                                id obj=nil;
                                NSEnumerator *en;
                                NSSize sz;
                            
        [ [NSColor whiteColor] set];
                                NSRectFill(rect);
                            
        sz = [self convertSize:NSMakeSize(1,1) fromView:nil];
                                [NSBezierPath setDefaultLineWidth:MAX(sz.width,sz.height)];
                            
        en = [displayList objectEnumerator];
                                while (obj = [en nextObject]) {
                                    if (NSIntersectsRect(rect,[obj bounds]) ) {
                                        [obj stroke];
                                    }
                                }
                            }

Note that for the first time we are using the drawRect: argument rect for more than simply drawing a rectangle: we use it to determine the intersection of GraphView and the object (line segment) to be drawn. The drawRect: method that we constructed for the PolygonView class in the previous chapter drew the entire polygon every time the method was invoked. That was okay because drawing the polygon involved very few drawing operations, but when drawing complex images, it’s wasteful to redraw the entire image — especially if you need to redraw only a tiny sliver of the image.

We’ll use drawRect:’s rect argument to help us determine which part of the screen to redraw. rect is passed as an argument to the Cocoa function NSIntersectsRect( ), which provides a handy way to determine if rect and the new area to be drawn intersect.

NSIntersectsRect( ) is one of the many Cocoa utility rectangle functions. Similar functions will tell you if one rectangle contains another rectangle or a specified point, or if two rectangles are the same. The function NSUnionRect( ) will compute the smallest rectangle large enough to contain two other rectangles, while the function NSIntersectionRect( ) will compute the region of overlap.

That’s it for the drawRect: method. This simple method is not only optimized to redraw the absolute minimum amount of the graph that’s ever required; it will also handle printing, faxing, and generating PDF files.

The Data Stuffer Methods

The part of GraphView that sets up and uses the data stuffer thread consists of the following four methods:

- (void)graph:(id)sender

Sets up global variables and starts the data stuffer thread.

- (void)stopGraph:(id)sender

Lets a user interrupt the graph currently being drawn.

- (void)sendData

The data stuffer method that will be executed in a separate thread by the NSThread class.

- (void)doStop:(int)which

The common logic for stopping the graph and resetting the GUI. This method is invoked regardless of whether the graph stops normally or by user intervention.

  1. Insert the lines shown here in bold into the graph: action method in GraphView.m:

    - (IBAction)graph:(id)sender
    {   // Set instance variables from the form
                               [self getFormAndScaleView];
                            
       // Check the parameters of the graph
                               if (xmax < xmin || ymax < ymin) {
                                    NSRunAlertPanel( nil, @"Invalid min/max combination",  
                                                     @"OK", nil, nil);
                                   return;
                                }
                            
        if ( xstep <= 0 ) {
                                    NSRunAlertPanel(0, @"The step size must be positive",
                                                    @"OK", nil, nil);
                                    return;
                                }
                            
        [self clear];
                            
        first = YES;
                                stop_sending = NO;
                                sending = YES;
                                receiving = YES;
                            
        [graphButton setTitle:@"Stop"];
                                [graphButton setAction:@selector(stopGraph:)];
                            
        [NSThread detachNewThreadSelector:@selector(sendData)
                                                         toTarget:self
                                                       withObject:nil];
    }

The graph: action method first validates the values entered by the user for the graphing parameters xmin, xmax, ymin, ymax, and xstep. If these values are not acceptable, an alert panel is displayed and the method returns.

If the values are acceptable, the display list is cleared and the state variables are initialized. The statement first=YES sets the first instance variable so that the method that builds the graph will know that a new graph is being created. The statement stop_sending=NO resets the instance variable that is used to control the stuffer thread. The sending=YES and receiving=YES statements set toggles that will be used in the doStop: and gotData: methods described a bit later.

The setTitle: message changes the title of the on-screen button from “Graph” to “Stop”. The related statement that follows changes the action method associated with the button from graph: to stopGraph:. If the user clicks the button when its title is “Stop”, the stopGraph: message is sent to the GraphView. This is the way to rewire (change the connection in) an application while it is running. You can’t do that in IB.

The last line in the graph: action method sends a message to the NSThread class to detach the stuffer thread. Although we haven’t seen it yet, the thread starts up with the sendData message being sent to the same GraphView object that was previously running. The trick here, of course, is that the sendData method executes simultaneously with the rest of the GraphView object.

  1. Insert the following sendData method into GraphView.m:

                            - (void)sendData
                            {
                                NSAutoreleasePool *threadPool = [ [NSAutoreleasePool alloc] init];
                                NSString *formula;
                                double x;
                                int i;
                            
        formula = [formulaField stringValue];
                            
        for (x=xmin; stop_sending==NO && x<=xmax; x+=xstep) {
                            
            NSMutableString *fsend = 
                                      [NSMutableString stringWithString:@"x,"];
                                    NSString *xString = [NSString stringWithFormat:@"%g",x];
                            
            [fsend appendString:formula];
                                    [fsend appendString:@"
    "];
                            
            // Now go through the formula and change every 'x' to a '%g' 
                            
            for (i=[fsend length]-1; i>=0; i--) {
                                        if ([fsend characterAtIndex:i] == 'x') {
                                            [fsend replaceCharactersInRange:NSMakeRange(i,1)
                                                                 withString:xString];
                                        }
                                    }
                            
            // Send this to the other side 
                                    [toEvaluator writeData:
                                           [fsend dataUsingEncoding:NSASCIIStringEncoding
                                               allowLossyConversion:YES] ];
                                }
                            
        // Now send through the termination code
                                [toEvaluator writeData:[@"999
    "
                                     dataUsingEncoding:NSASCIIStringEncoding
                                  allowLossyConversion:YES] ];
                                [self doStop:STOP_SENDER];
                            
        // Release the pool before the thread exits
                                [threadPool release];
                            }

The sendData method implements the entire stuffer thread, so it is understandably complicated (we hope that you’ll find it understandable as well!). The first thing this thread does is set up its own NSAutoreleasePool. Each thread must have its own autorelease pool: it would do no good to have one thread’s releasing another thread’s data!

After the autorelease pool is set up, the sendData method makes a copy of the formula that is presently in the GraphView’s formulaField. It then sets up a loop that will step the variable x from xmin to xmax by xstep (recall that these instance variables were set up by the message [self getFormAndScaleView] in the graph: method. Each time through the loop, the method creates a new NSMutableString that contains the formula that is to be solved. The x variables are then replaced with the current value of x. This algebraic formula is then sent to Evaluator.

When the loop finishes, the data stuffer sends the number 999 to Evaluator. This number is used as a flag to indicate that no more data is coming through the pipe. The procedure that constructs the graph will look for a 999 on a line by itself and will use that flag as its way of knowing that the graph is finished. The specific digits 999 really don’t matter: what’s important is that Evaluator is sent a line of data with one expression and no comma.

When the whole process is finished, the doStop: method is invoked with the argument STOP_SENDER to indicate that the stuffer has finished. Finally, the autorelease pool is released, which causes all of the temporary strings that were created to be freed.

Stopping a Running Graph

The stopGraph: method stops a running graph. It is invoked when the user clicks the Stop button (“Stop” replaces “Graph” as the button’s title only when a graph is being drawn).

  1. Insert the line shown here in bold into the stopGraph: method in GraphView.m:

    - (IBAction)stopGraph:(id)sender
    {    stop_sending = YES;
    }

As part of its main loop, the data stuffer monitors the status of the stop_sending Boolean variable. When this variable is set to YES, the data stuffer immediately stops what it is doing and sends the termination code 999 to Evaluator.

The doStop: method is invoked twice, once when the stuffer stops sending, and again when Evaluator receives the stuffer’s termination code, which is the last line that the stuffer sends prior to terminating.

  1. Insert the following doStop: method into GraphView.m:

                            - (void)doStop:(int)which
                            {
                                switch (which) {
                                   case STOP_SENDER:
                                        sending = NO;
                                        break;
                                   case STOP_RECEIVER:
                                        receiving = NO;
                                        break;
                                }
                            
        if (sending==NO && receiving==NO) {  // Reinitialize
                                    [graphButton setTitle:@"Graph"];
                                    [graphButton setAction:@selector(graph:)];
                                    [graphButton setEnabled:YES];
                                }
                            
        if (sending==NO && receiving==YES) { // Wait for results data
                                    [graphButton setEnabled:FALSE];
                                    [graphButton setTitle:@"Waiting..."];
                                }
                                if (sending==YES && receiving==NO) { // A problem
                                    NSLog(@"Synchronization error");
                                }
                            }

This doStop: method controls the on-screen Graph button. When the user first clicks the Graph button, its label changes from “Graph” to “Stop”. Pressing the button when it is labeled “Stop” causes the stop_sending flag to be set, as discussed earlier. But after the stuffer flag has finished sending its data, neither “Graph” nor “Stop” is really an appropriate setting for this button. Instead, there is a third mode: the button displays “Waiting . . . " and is disabled. At this point, the application is simply waiting for Evaluator to process all of the information that it has been sent and to return the calculated results.

The Graph Displayer

Now it’s time to implement the method that receives data from Evaluator and constructs line segments that make up the graph.

  1. Insert the following gotData: method into GraphView.m:

                            - (void)gotData:(NSNotification *)not
                            {
                                NSData      *data;
                                NSString    *str;
                                NSPoint      pt;
                                int          num;
                                NSString    *line=0;
                            
        data = [ [not userInfo]
                                          objectForKey:NSFileHandleNotificationDataItem];
                                str  = [ [NSString alloc] initWithData:data
                                                              encoding:NSASCIIStringEncoding];
                            
        // Add the data to the end of the text buffer
                                [fromBuf appendString:str];
                            
        // Register to get the notification again
                                [fromEvaluator readInBackgroundAndNotify];
                            
        // Now, process all complete lines we have
                                do {
                                    NSRange r1;
                            
            r1 = [fromBuf rangeOfString:@"
    "];
                                    if (r1.length<1) break;
                            
            line = [fromBuf substringToIndex:r1.location];
                                    [fromBuf
                                         replaceCharactersInRange:NSMakeRange(0,r1.location+1)
                                                       withString:@""];
                            
            num = sscanf( [line cString], "%f, %f", &pt.x, &pt.y);
                                    if (num!=2) {
                                        [self doStop:STOP_RECEIVER];
                                        return;
                                    }
                            
            if (!first && !stop_sending) {
                                        Segment *seg = [ [ [Segment alloc] 
                                                            initFrom:lastPt to:pt ] autorelease];
                                        [seg setTag:GRAPH_TAG];
                                        [self addGraphElement:seg];
                                    }
                            
            first = NO;               // No longer first
                                    lastPt = pt;              // Remember this point
                            
        } while (line);
                                // End of data
                            }

This method is invoked whenever new data is available. Its main complication is that it needs to break the block of data it receives into individual lines. Each of these lines is then used to create a Segment object, and these objects are then added to the display list with the addGraphElement: method.

The reason for the line-by-line buffering is that Evaluator might send more than one line of data to the GraphView object before it is scheduled to read the data (because the data is being generated by a different execution thread). It might also send an incomplete line, due to blocking on the pipe. The GraphView object therefore needs to buffer the data that it receives and then read it out one line at a time.

Note that the addBufToGraph: method ignores the data it receives if the stop_sending instance variable is set to YES. This means that after the user clicks the Stop button all of the rest of the data in the pipeline is ignored, giving the application a nice snappy response time.

The gotData: method uses the sscanf( ) function to turn the line of text from Evaluator back into numbers. If num!=2, then there were not two numbers separated by a comma to read; in this case, the method invokes the stopGraph: method and the graph stops.

If this data pair is the first data pair, the execution drops down to the last four lines. These lines set the instance variables lastx and lasty to be the coordinates of the current point, then unsets the first variable and returns.

On all data pairs other than the first, the middle section of this method gets executed. This conditional code first creates a Segment object (described in the next section) with endpoints at (lastPt.x, lastPt.y) and (pt.x, pt.y) and adds this segment to the display list.

This method also sets the tag of the segment to GRAPH_TAG. We’ll use tags later to distinguish between different objects stored inside the display list.

The Segment Class

Although a GraphView object constructs graphs, it relies upon a Segment object to actually draw the lines that make up the graph. The GraphView object invokes two Segment instance methods:

bounds

Returns a rectangle bounding the Segment’s line

stroke

Causes the Segment object to draw itself in the current view

By using a separate class that interacts with the GraphView class according to a well-defined protocol, we open up the possibility of adding new objects to the graph with very little work. To make the Segment class even more general, it supports a tag internal variable (which we’ll use later, when we add more types of objects).

  1. Choose PB’s File New File command, select Cocoa Objective-C class, and click Next.

  2. Name the file Segment.m. Leave the checkbox checked so that Segment.h is also created in the ~/GraphPaper folder, GraphPaper project, and GraphPaper target. Click Finish.

  3. Edit the Segment.h file so that it looks like the following:

                            #import <Cocoa/Cocoa.h>
                            #import "GraphView.h"
                            
    @interface Segment:NSObject <GraphViewElement>
                            {
                                NSPoint     start;
                                NSPoint     end;
                                NSColor    *color;
                                int         tag;
                            }
                            
    - initFrom:(NSPoint)start to:(NSPoint)end;
                            - (NSPoint) segmentCenter;
                            @end

The @interface directive with the angle brackets (<>) tells the Objective-C compiler that Segment is a subclass of NSObject that follows the GraphViewElement protocol, and thus that it must implement the six methods declared previously in that protocol.

  1. Edit the Segment.m file so that it looks like the following:

                            #import "Segment.h"
                            
    @implementation Segment
                            
    - initFrom:(NSPoint)theStart to:(NSPoint)theEnd 
                            {
                                [super init];         // Init the NSObject superclass
                            
        start = theStart;
                                end   = theEnd;
                                color = [ [NSColor blackColor] retain];
                                return self;
                            }
                            
    - (void)dealloc
                            {
                                [color release];  // Release what you retain
                                [super dealloc];  // and dealloc the superclass
                            }
                            
    // Accessor methods
                            - (int)tag      { return tag; }
                            - (void)setTag:(int)aTag
                            {
                                tag = aTag;
                            }
                            
    - (void)setColor:(NSColor *)aColor
                            {
                                [color release];
                                color = [aColor retain];
                            }
                            
    - (NSColor *)color
                            {
                                return color;
                            }
                            
    // Methods that derive information for the caller
                            - (NSRect)bounds
                            {
                                return NSMakeRect( MIN(start.x,end.x),
                                                   MIN(start.y,end.y),
                                                   fabs(start.x-end.x) + FLT_MIN,
                                                   fabs(start.y-end.y) + FLT_MIN );
                            }
                            
    - (NSPoint)segmentCenter
                            {
                                return NSMakePoint((start.x+end.x)/2.0, (start.y+end.y)/2.0);
                            }
                            
    - (void)stroke
                            {
                                [color set];
                                [NSBezierPath strokeLineFromPoint:start toPoint:end];
                            }
                            
    @end

The Segment class implementation is fairly straightforward. Notice that there is no bounds instance variable; instead, we calculate each segment’s bounding box on demand from other instance variables and return what was calculated. This is known as data hiding — an object’s internal representation of data does not have to be the same representation that is used by its accessor methods.

We use FLT_MIN in the bounds method so that lines that are vertical or horizontal will still have a width or height that is non-zero. FLT_MIN is the smallest floating-point number that the IEEE floating-point package can represent. By adding FLT_MIN to the calculated width and height, we guarantee that these values will not be zero. If they are computed to be a number that is larger than FLT_MIN — for example, the number 5 — adding FLT_MIN will have no significant effect, as it’s a very tiny value.

Testing GraphPaper

Now that we’ve built the interface, made the connections, and implemented all the classes, we’re finally ready to make and test GraphPaper.

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

  2. With GraphPaper running, click the Graph button. You’ll see the graph of y=sin(3*x) over the x range [0,10], as shown in Figure 16-4. Because xstep=0.1, 100 line segments (steps) made up the graph of sin(3*x).

Graphing sin(3*x) with GraphPaper

Figure 16-4. Graphing sin(3*x) with GraphPaper

  1. Change the value of xstep to 0.001 and click the Graph button again. This time, the graph of sin(3*x) will be displayed slowly, and the Graph button title will change to “Stop” and then a dimmed “Waiting . . . “. Try clicking the Stop button.

  2. Try graphing another function, such as x*cos(4*x), with a different step and ranges.

  3. Try entering a negative value for xstep and click the Graph button. An alert should show up. Try entering min values greater than the max values, and you should get another alert.

  4. Quit GraphPaper.

In Chapter 18, we’ll clean up the GraphPaper application a bit and make it respond properly to resizing, and we’ll arrange for the (x,y) coordinates of each point to be displayed as the mouse is moved over the graph. We’ll finish off this chapter by showing how to add two different objects to GraphPaper’s display list.

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

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