Before we can save the contents of a MathPaper window into a file, we first need to know what kind of file our application will create. Specifically, we need to know the file’s HFS creator code, its HFS type code, and its file extension.
Creator codes
are used so that when you double-click on
a filename in the Finder, Cocoa knows to open the correct
application. Creator codes are also used to locate applications on
the computer’s hard disk. You may remember that, in Section 10.4.5, we were assigned by Apple a
creator code for the MathPaper application. That creator code was
MATP
.
Because a single application can create many different kinds of files, and because multiple applications are sometimes able to read the same file, document files stored on HFS filesystems also have a type code . Like creator codes, file type codes are 4-byte strings or 32-bit quantities. In many cases, the file type is the same as the file’s extension in the world of Windows and Unix: GIF and JPEG are used for image files, for example.
In addition to creator codes, Mac OS X uses file extensions to match up applications and their document files. A file extension is the set of letters that come after the last period (dot) in a file’s name. Unlike the Mac OS 9 Finder, the Mac OS X Finder makes it somewhat difficult for the user to accidentally change a file extension. It does this by generally hiding file extensions and by warning the user before an extension is changed. Mac OS relies on file extensions more and more as time goes on, which generally improves interoperability with other platforms that do not have creator and type codes.
Many of the commonly used extensions and their meanings are listed in Table 13-1. These are some of the standard extensions used by Mac OS X.
Table 13-1. Common Mac OS X file extensions
File extension |
File type |
---|---|
|
Unix library file |
|
Directory containing an application |
|
C-language source file |
|
Encapsulated PostScript file |
|
Graphics Interchange Format file |
|
C (or Objective-C) header file |
|
Joint Photographic Expert’s Group format file |
|
|
|
Directory containing language-specific nibs |
|
Objective-C language source file |
|
Directory containing a |
|
File containing binary MIDI data |
|
MPEG2 Level 3 audio file |
|
IB file |
|
Unix object code file |
|
Portable Document Format file |
|
Project file for use with PB |
|
Rich Text Format file |
|
Rich Text Format directory containing |
|
File containing assembler source |
|
Tape Archive format file (see the Unix manpage for
|
|
Tagged Image File Format file |
|
Plain text file |
|
Uuencoded file |
|
|
|
File that has been compressed |
For our application, we’ll use the string
"
MATP” as the MathPaper
file type and the extension .matp
for MathPaper
document files. Now we’ll set up icons.
The icon should be saved in the
following four resolutions: 16 x 16, 32 x 32,
48 x 48, and 128 x 128 pixels. If you want to
make only one icon, make the 128 x 128 icon and let the
system scale the others. We came up with the icon shown here. (A quick way to create an icon is to use the Grab
application to grab a small screen-shot selection and save it in the
TIFF format. Grab is located in the
/Applications/Utilities
folder.)
In IconComposer, create a PaperIcon.icns
file
from your icon files. Refer to Section 6.3.2 for details on using the IconComposer program.
Open MathPaper.pbproj
in PB.
Choose PB’s Project → Add Files
menu command and add the PaperIcon.icns
file to
the Resources group in the Groups & Files pane (or simply drag
the file from the Finder and drop it in the Resources group in PB).
Make sure that you add PaperIcon.icns
to the
MathPaper target.
Select the Targets vertical tab, then click on the MathPaper target.
Click on the newly visible Application Settings tab, scroll down to the Document Types section, and select the MathPaper document type.
Enter PaperIcon.icns
in the Icon file field and
click the Change (not Add) button. The pertinent part of
PB’s main window can be seen in Figure 13-2.
At this point, PB knows that MathPaper can handle only one kind of
document — a MathPaper document that has the extension
.matp
and the HFS type code
“MATP”. MathPaper is an Editor for
this file type, meaning that it can both read and write these files.
Saving the contents of a window means writing all of the states associated with the window into a file, so that we can reconstruct the window’s current state as closely as possible when it’s reloaded. In the case of a MathPaper window, not many states need to be saved. Because the Evaluator back end doesn’t retain state between launches, the only information that we need to save in a MathPaper document file is:
The current location of the window (the frame)
The window’s history
This is precisely the information that we decided to store in the MathDocument instance variables — how convenient! To implement file saving, all we need to do is to write the code to migrate this information from each window’s PaperController object to its matching MathDocument object, then arrange for the pertinent instance variables to be saved in a file on disk.
In Step 4 in the previous section, we modified
the windowDidLoad method in
PaperController.h
so that the MathPaper
window’s initial frame and the contents of the
NSTextView would be taken from the MathDocument object. To save a
MathPaper document to a file, we need to perform the reverse
operation — we need to copy the contents of the NSTextView object
and the window’s frame back into the MathDocument
instance variables. That’s what the new
PaperController synchronizeData method shown below will do.
Insert the new synchronizeData
method declaration shown here in bold into the
PaperController.h
file:
#import <Cocoa/Cocoa.h>
@interface PaperController : NSWindowController
{
NSTask *evaluator;
NSPipe *toPipe;
NSPipe *fromPipe;
NSFileHandle *toEvaluator;
NSFileHandle *fromEvaluator;
IBOutlet NSTextView *theText;
}- (void)synchronizeData;
@end
Add the synchronizeData method
implementation to the PaperController.m
file:
- (void)synchronizeData { NSRange allRange = NSMakeRange(0,[ [theText textStorage] length]); MathDocument *doc = [self document]; [doc setHistory:[theText RTFFromRange:allRange] ]; [doc setFrame:[ [self window] frame] ]; } ... @end
This method gets a copy of the RTF data in the NSTextView object in
an NSData object. The MathDocument’s history object
is then set to be equal to this RTF data. Following that, the
MathDocument’s setFrame:
method is invoked to set the frame
of
the PaperController’s window.
Merely copying the data out of the PaperController’s objects and into the MathDocument is not sufficient; to save this data on the hard disk, we need to create a method that will turn this information into a byte stream.
To create that byte stream, we will use Cocoa’s NSCoder, NSArchiver, and NSUnarchiver classes. These classes are responsible for archiving and restoring data. With these classes, you can archive an object to a byte stream that is stored in an NSData object with a single method invocation, as follows:
NSData *theData = [NSArchiver archiveDataWithRootObject:anObject];
After this message is sent, the NSArchiver class creates an NSCoder instance that is responsible for doing the actual encoding. The NSCoder instance then invokes a version of theencodeWithCoder: method that we create, passing a pointer to itself as the argument.
Thus, to operate with the NSCoder system, our MathDocument class
needs to implement the encodeWithCoder: method. This method is quite
simple; it usually consists of a message to the
coder
object argument for each instance variable
that needs to be archived. For example, if you had a class with a
single instance variable called frame
, your
encodeWithCoder: method would look
like this:
-(void)encodeWithCoder:(NSCoder *)coder { [coder encodeRect:frame]; }
The real power of the NSCoder system comes when you are archiving complicated objects that contain references to many other objects. Each object that is archived automatically archives all of its subobjects, resulting in an entire tree or graph of objects being archived in the NSData object. The system transparently handles objects that are referenced in more than one location, as well as circular references.
If you casually read the NSCoder documentation or the
NSCoder.h
include file, you may feel that the
NSCoder system is powerful but somewhat difficult to use.
That’s because the NSCoder class just defines the
base methods that are used for writing out raw data, byte arrays, and
objects. There are many higher-level NSCoder methods that are defined
as category methods in other include files, such as
NSGeometry.h
. Table 13-2
contains a list of many of the NSCoder methods that you may use in
creating your applications.
Table 13-2. Common methods in the NSCoder class
Method |
Purpose |
---|---|
- (void)encodeObject:(id)object |
Encodes an object |
- (void)encodePropertyList:(id)aPropertyList |
Encodes a property list |
- (void)encodePoint:(NSPoint)aPoint |
Encodes an NSPoint structure |
- (void)encodeSize:(NSSize)aSize |
Encodes an NSSize structure |
- (void)encodeRect:(NSRect)aRect |
Encodes an NSRect structure |
- (void)encodeDataObject:(NSData *)data |
Encodes an NSData object |
- (void)encodeValuesOfObjCTypes:@encode(int)i |
Encodes an integer |
- (void)encodeValuesOfObjCTypes:@encode(float)f |
Encodes a floating-point value |
- (void)encodeArrayOfObjcCTypes:@encode(type) count:(unsigned)aCount at:(void *)addr |
Encodes an array of type type |
To use the NSCoder class, we will create an encodeWithCoder: method for MathDocument:
Many implementations of the encodeWithCoder: method will invoke their superclass’s encodeWithCoder: method (e.g., [super encodeWithCoder:coder] ) so that the instance variables of the superclass are automatically encoded. This is not the correct approach with subclasses of the NSDocument class, however, because we do not actually want the instance variables of the NSDocument class stored in the byte stream. (Another reason not to send the [super encodeWithCoder:coder] message is that the NSDocument class does not implement the encodeWithCoder: method, so calling the super method would result in an error.)
We have created one method for copying the state of each MathPaper window into the MathDocument object and another method for copying these instance variables into an NSData object. All that remains for us to do to implement file saving is to display the appropriate Save panel when the user chooses File → Save (or types Command-S). The Save panel will prompt for a filename, get the filename, get the NSData for the instance variables, and put all of this information into a file. Fortunately, we need to write only five lines of code to implement this functionality; Cocoa gives almost all of it to us for free.
To understand how all this happens, we’ll first focus on the First Responder icon that we first discussed back in Chapter 3. We’ve already discussed the First Responder in the context of the File menu items. Now we’re going to find out what it means.
The First Responder icon in the Nib File window is a placeholder for the application’s current first responder; that is, the object that will be the first to try to respond to keyboard events and menu commands such as Cut, Copy, and Paste. Any message sent to the First Responder icon is sent in order to each of the following objects, until an object is found that can receive the message and respond with a value other than nil:
The key window
The key window’s delegate
The key window’s NSWindowController (if you are using the document application framework)
The key window’s NSDocument
The application’s main window
The application’s main window’s delegate
The main window’s NSWindowController
The main window’s NSDocument
The application’s NSApplication object
The application’s NSApplication object’s delegate
The application’s NSDocumentController
If you open MathPaper’s
MainMenu.nib
file in IB and choose the File
→ Save menu item, you will see that it sends the
saveDocument:
action message to the First Responder
icon, as shown in Figure 13-3. (Note that we saw a
similar premade connection from File → New in Chapter 10.)
Thus, when a user chooses the File → Save menu command in a running MathPaper program, the saveDocument: message will be sent to the MathDocument object that controls the key (active) window. Cocoa implements the saveDocument: method in the NSDocument class. The behavior of this method depends on whether or not a filename has been assigned to the document. If a filename has not been assigned, the NSDocument class runs a modal Save panel to get a document and type. (A modal Save panel requires user action and must be dismissed before anything else can be done in the application.) After the user enters an appropriate name, etc., the document is saved and its “edited” status is cleared.
After the NSDocument class gets the filename, it needs to create an NSData object to contain the archived instance variables of your class. This NSData then needs to be written into a file on disk. The NSDocument architecture gives you not one, but three different ways for providing this information in your subclass, as well as for reading back the files from disk after they are created:
If you simply want to be able to save a document to disk and load it back in, all you need to do in your NSDocument subclass is to override the following two methods:
This method returns an NSData object that contains a sequence of bytes that corresponds to the document currently in memory. Some documents can be stored as multiple types, so you should check the NSString argument and provide the requested type.
This method is passed an NSData object that contains the sequence of bytes that you should load, and an NSString that contains the document type that you need to read in. Your method should load the NSData object into the NSDocument’s memory and return TRUE if it’s successful or FALSE if it fails. The NSString argument is useful if your application knows how to load more than one kind of file.
For some kinds of applications, it is easier to store a document in a file wrapper that consists of a folder and multiple files. If you want to store the contents of your document in a file wrapper, override these two methods:
This method should return an NSFileWrapper object that corresponds to the document’s contents.
This method loads the current document from the NSFileWrapper wrapper.
If you need still more control over the saving and loading of your documents, you can override these four methods:
(BOOL)writeToFile:(NSString *)fileName ofType:(NSString *)type; |
(BOOL)writeToURL:(NSURL *)url ofType:(NSString *)type; |
(BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type; |
(BOOL)readFromURL:(NSURL *)url ofType:(NSString *)type; |
The NSDocument class contains additional methods that you can use to control whether or not backup files are preserved, if you need to access the previous version of a file when you are saving a new version (for example, if the entire document cannot fit in memory) or if your “revert” action needs to have some sort of specific behavior. As with all classes discussed in this book, you can consult the Cocoa documentation for a further explanation of the methods mentioned here, as well as additional methods that we have not described.
To help implement file saving in MathPaper, we will override NSDocument’s dataRepresentationOfType: method in our MathDocument subclass. Our implementation will get a list of all the PaperController objects associated with the current MathDocument document (object), send each of those objects a synchronizeData message, and then return the NSData object that results when an NSArchiver is asked to archive the MathDocument object.
Insert the new code shown here in bold into the dataRepresentationOfType: method in the
MathDocument.m
file:
- (NSData *)dataRepresentationOfType:(NSString *)aType { if ([aType isEqualToString:@"MathPaper"]) { // Ask our windows to synchronize their data [ [self windowControllers] makeObjectsPerformSelector:@selector(synchronizeData)]; // Encode the data return [NSArchiver archivedDataWithRootObject:self]; } return nil; // Cannot encode }
The “list of all the PaperControllers” approach may seem like overkill, because we know that our application has only one PaperController per MathDocument window. Indeed, the second statement could have been written like this:
... // Ask our one window to synchronize its data [[ [self windowControllers] objectAtIndex:0]] synchronizeData]; ...
But because the NSDocument architecture returns an
NSArray
*
reference when we
send it the windowControllers
message, it’s actually easier to write the
generalized code that sends the synchronizeData message to every object in the
NSArray (even though we know that only a single element is there).
With this code, if a second NSWindowController instance is ever added
to the MathPaper application — for example, if we should want to
have two windows for every MathDocument — we’ll
be prepared. See Section 13.7 at the end of the chapter for more on this topic.
After you’ve made the changes, it’s time to test them.
Build and run MathPaper. Save all pertinent files before building.
Create four MathPaper windows and drag them to the four corners of your screen.
Type some equations in each of the four windows.
Choose the MathPaper → Save (or Save As) menu command for each window. Because you haven’t given this window a filename yet, both functions do the same thing — bring up the Save panel. Note that the application icon automatically appears in the Save panel.
We’ll save each window with a name such as “upper-left”, “lower-left”, “upper-right”, and “lower-right”, as shown in Figure 13-4. If necessary, click Home in the Save panel to open your Home directory. Then type a filename and click OK (or hit Return) to save the file in your Home directory. The document icon will appear above your chosen filename in your File Viewer, as shown in Figure 13-5.
Quit MathPaper.
Note that each time you save a file, the title of the window bar changes. If you look closely, you’ll also see that each window has the MathPaper document icon in its title bar, as shown in Figure 13-4.
The document icon that you’ve created should also
show up in the Finder, as it does in Figure 13-5. If
the document icon does not show up, log out of your computer and log
back in — the Mac OS X Finder doesn’t always
recognize new icons when they are manufactured by applications under
development. If that doesn’t work, try putting a
copy of the MathPaper.app
application icon in
your Dock.
The NSDocument architecture automatically adds the file extension
.matp
to the documents that we create. You can
use the Finder’s Show Info panel (Command-I) to
control whether the extension is shown or displayed on a file-by-file
basis.
There may be circumstances in which the Save panel that is produced by the NSDocument architecture is not sufficient for what you wish to do. For example, you may wish to have a radio button or a checkbox on the Save panel. Cocoa makes it easy to customize both Save panels and Open panels with your own accessory views. We won’t need such customization here, but this is a good place to provide an overview of how it’s done.
When the NSDocument class (or your subclass) attempts to invoke saveDocument: and no filename is specified, or when the user asks to do a Save As or a Save To operation, the following methods in your NSDocument subclass are called, in this order:
(void)runModalSavePanelForSaveOperation:
(NSSaveOperationType)saveOperation
delegate:(id)delegate
didSaveSelector:(SEL)didSaveSelector
contextInfo:(void *)contextInfo
(BOOL)prepareSavePanel:(NSSavePanel *)savePanel
(void)saveToFile:(NSString *)fileName
saveOperation:(NSSaveOperationType)saveOperation
delegate:(id)delegate
didSaveSelector:(SEL)didSaveSelector
contextInfo:(void *)contextInfo
(NSString *)fileTypeFromLastRunSavePanel
So, if you want to have a custom control that specifies the file type for a file to be saved, you can follow this sequence of steps:
Subclass prepareSavePanel: so that an “accessory view” containing the control is inserted into the Save panel.
Subclass fileTypeFromLastRunSavePanel to return the file type that the user chose.
Arrange for your dataRepresentationOfType: method to examine the type that is passed in and save the file according to the specified format. Your method could also examine the settings of other controls on the auxiliary view if they can be used to change the format of the saved file. (For example, the accessory view might be used in a program that saves JPEG files to specify a compression setting.)
3.145.199.112