In the previous chapter we saw how the contents of a text object can be translated into a Rich Text stream of characters. In this chapter, we’ll see how to take that stream and save it into a file; we’ll also see how to load one of those files and place its contents in a new window. Finally, we’ll learn about printing. All of these tasks will be made dramatically easier by using the Cocoa Application Kit framework.
At this point, our MathPaper application does a great job with math, fonts, and handling multiple windows. However, it’s missing a lot of basic functionality, such as:
Saving the contents of a window into a file
Loading a saved file, so that you can continue calculating where you left off
Marking an edited window with the “unsaved” close button with a dot inside so that you know it has been edited (the “saved” close button has an X inside:
)
Alerting a user who tries to close an edited window without first saving the edited file
Printing the contents of a window
Graying out menu items that are not appropriate in a given context (e.g., the Save menu item when there are no open documents)
Writing the code for document saving and loading can be quite an ordeal in some computing platforms. However, it’s easy in Cocoa because most of the required code is already part of the multiple-document framework.
When you save a document to a file on your computer’s hard drive, you are making a representation of a part of the computer’s memory and archiving that representation in a way that can be restored later. In some cases, the easiest representation is a byte-for-byte copy of the computer’s memory. Applications that use this technique are simply storing a memory snapshot on disk.
Other applications take a more intelligent approach to archiving information. For example, a vector-graphics drawing program stores the endpoints of a line rather than the positions of all of the individual pixels that make up the line. Also, an object-oriented drawing program might archive a document to disk by telling its document object to write a stream of bytes to the disk containing the values in all of the instance variables. This command would then be recursively applied to all of the objects that the document contains. In this manner, all of the text, images, lines, and other information that can be referenced from the document would be written out to disk.
There are other ways that applications can save documents to disk as well. Microsoft Word, for instance, has a feature called fast saves . If you open a large document with Word, make a few changes, and then try to save it, Word can simply append the change journal to the end of the first document, rather than making a whole new copy. Fast saves can be a real timesaver, but they have the disadvantage that the unmodified text remains in the document — which can produce embarrassing situations at times! Early versions of Word were also somewhat buggy, and occasionally fast saves caused document files to become corrupted.
Let’s review where we are with our MathPaper program. Our MathDocument class is a subclass of NSDocument, and our PaperController class is a subclass of NSWindowController. There is also a single NSDocumentController object in our program that’s mostly invisible to the programmer, even though it has overall control of the multiple-document architecture and does a lot for both the interface and the program. For example, this NSDocumentController object automatically creates a new MathDocument object when MathPaper launches and initializes another new MathDocument object every time a user chooses File → New from MathPaper’s menu. NSDocumentController also maintains a list of NSDocument (i.e., MathDocument) objects and all of their NSWindowControllers. In addition, this object manages the on-screen document windows and much of the main menu.
MathDocument’s makeWindowControllers
method is automatically invoked at
MathPaper launch time and when File → New is chosen.
Our override of makeWindowControllers in the previous chapter
creates a new PaperController object, unarchives
PaperWindow.nib
with the new PaperController
object as its owner, and adds the new object to the list of window
controllers.
Note that NSDocumentController implements thesaveAllDocuments: , newDocument: , and openDocument: action methods associated with the File menu commands. These methods are all connected to the First Responder icon, as you can see in Interface Builder.
Our MathDocument class doesn’t do much yet. At this point, it responds to makeWindowControllers and windowNibName messages but otherwise sits dormant. All of the controls, the state information, and the Evaluator hookup for each window are kept in the associated copy of PaperController. What is the state information? For each MathPaper window, the following information is relevant:
The current location and size of that MathPaper window (called the window frame)
The window’s history — that is, the calculations that have been displayed
To this end, we need to redesign the way that data is kept in the application. For each MathPaper window, the MathDocument object will be responsible for keeping the “official” copy of the data, and the PaperController object will be responsible for keeping the “working copy” — that is, the copy the user is modifying.
Our changes to the MathPaper class will
consist of adding two instance variables and five accessor methods.
The two instance variables, frame
and
history
, will keep track of the
MathDocument’s window frame and its math-expression
history. The accessor methods allow us to set and inspect these
variables. Later, we’ll also modify the
PaperController class so that it takes its data from the MathDocument
class.
We’ll also add a new initializer to the MathDocument
class that will initialize the history
variable to
a default value. In this case, we’ll choose a
default that improves the usability of the MathPaper application by
adding a line of instruction on how to use the application.
Back in Project Builder, insert the lines shown here in bold into the
MathDocument.h
class interface file:
#import <Cocoa/Cocoa.h> @interface MathDocument : NSDocument { NSRect frame; NSMutableData *history; }-(NSData *)history; -(void)setHistory:(NSData *)theHistory; -(NSRect)frame; -(void)setFrame:(NSRect)aFrame; -(BOOL)hasFrame; @end
With the exception of hasFrame, the
names of these five methods indicate what they do. The hasFrame method tells the invoking object
whether the frame
instance variable has been set.
If it hasn’t, it still reads the
“factory default” value — that
is, all zeros.
Insert the #import
directive and add the methods
shown here in bold to the MathDocument.m
class
implementation file:
#import "MathDocument.h" #import "PaperController.h" @implementation MathDocument ...-(NSData *)history { return history; } -(void)setHistory:(NSData *)theHistory { [history setData:theHistory]; } -(NSRect)frame { return frame; } -(void)setFrame:(NSRect)aFrame { frame = aFrame; } -(BOOL)hasFrame { return (frame.size.height!=0 && frame.size.width!=0); } ...
As you can see, these methods are all pretty straightforward. The history and frame methods return their respective instance variables, while the setHistory: and setFrame: methods set them. The hasFrame method returns TRUE if the frame has been set (that is, if the size of the frame is nonzero) and FALSE if it has not been set.
We also need to create a designated initializer and deallocator for
the MathDocument class. The initializer will create the initial
NSMutableData object for the MathPaper history
instance variable and fill history
with its
initial content. The deallocator will release the storage that has
been allocated when it’s no longer needed.
Add the init and dealloc methods shown here in bold to the
MathDocument.m
class implementation file:
// Designated initializer; create the empty document - (id)init { RTF *rtf = [ [ [RTF alloc] init] autorelease]; [super init]; [rtf setBold:TRUE]; [rtf setSize:10.0]; [rtf appendString:@"Enter a math expression and hit return:"]; [rtf setSize:12.0]; [rtf appendString:@" "]; history = [ [NSMutableData alloc] initWithData:[rtf data] ]; return self; } -(void)dealloc { [history release]; [super dealloc]; }
Finally, we want to modify the PaperController’s
windowDidLoad method so that it
reads the value for the window’s
frame
and history
after the
PaperWindow.nib
file is loaded. With the code
that we’ve written, history
will
always be equal to the default value and frame
will never be set. That’s okay for now;
we’ll set the frame
value in the
next part of this chapter.
Insert the lines shown here in bold into the windowDidLoad method in the
PaperController.m
file:
#import "PaperController.h"; #import "RTF.h";#import "MathDocument.h"; ... - (void)windowDidLoad { NSString *path; MathDocument *doc; [super windowDidLoad]; [ [self window] makeFirstResponder:theText]; // Initialize with document doc = [self document]; [theText replaceCharactersInRange:NSMakeRange(0,0) withRTF:[doc history] ]; if ([doc hasFrame]) { [ [self window] setFrame:[doc frame] display:YES]; } path = [ [NSBundle mainBundle] pathForResource:@"Evaluator" ofType:@""]; toPipe = [NSPipe pipe]; // NSTask below will retain fromPipe = [NSPipe pipe]; // NSTask below will retain toEvaluator = [toPipe fileHandleForWriting]; fromEvaluator = [fromPipe fileHandleForReading]; evaluator = [[NSTask alloc] init]; [evaluator setLaunchPath:path]; [evaluator setStandardOutput:fromPipe]; [evaluator setStandardInput:toPipe]; [evaluator launch]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotData:) name:NSFileHandleReadCompletionNotification object:fromEvaluator]; [fromEvaluator readInBackgroundAndNotify]; }
Build and run the MathPaper application. Type Command-N a few times
to create a few windows. Each window should contain the brief
instructions that we’ll now create in
MathPaper’s designated initializer,
init
. (See Figure 13-1.)
Many simple document-based applications don’t bother with separate subclasses of both the NSDocument and NSWindowController classes. Instead, they simply put all of the document-management and window-control functionality inside the NSDocument subclass. This approach works fine until you want to display two different kinds of document windows — for example, plain text and RTF text windows in TextEdit, or two windows on a single document.
Because most Cocoa programmers will eventually need to subclass both the NSWindowController and NSDocument classes, we decided to simply start that way in our demonstration program. We hope it hasn’t been too confusing!
In the next section, we’ll build upon this framework by having Cocoa save the contents and the position of the window into a file.
18.216.27.251