You’ve now built an application that allows users to plan trips to the moon. Planning how to get somewhere is helpful, but what do tourists do once they get to the moon? To make your application truly useful as a moon travel planner, you should help users plan their itineraries once they’re moon-side. In this section, you will add a new window to your application: the itinerary window, which will display a traveler’s vacation event schedule for the duration of a stay on the moon.
You will also save the contents of the itinerary window in a file, and retrieve the contents of existing itinerary files. If you were creating a fully featured, robust application, you would make the text of the itinerary file editable by the user; however, that would involve creating a text editor. Since we do not show you how to handle text editing in this book, we will use a read-only itinerary file. Using a read-only file, we can still implement the Open and Save As commands. Once your application can handle these commands, you will have implemented most of the file handling code that you would need in order to implement the other standard File menu commands, Save and New. We will note in the chapter the places where you would normally implement these other commands.
To get a quick start on supporting text editing capabilities in your application, read the Multilingual Text Engine documentation in Carbon Help.
To add an itinerary to your Moon Travel application, you will:
Create a new itinerary window.
Define the constants and variables needed to create an itinerary file.
Write the window event handler for the itinerary window.
Write the functions to open an itinerary file.
Write the functions to save an itinerary file.
Write the function to close the itinerary window.
Add code to the main window event handler to open an itinerary.
Build, run, and test the application.
The first step in creating itineraries that your users can
actually use is creating an interface for the itinerary. Before
we do anything else, we need a window in which we can display the
itinerary. By now, you’re an old pro at creating windows. This
time, however, we’ll do things a bit differently. In Chapter 8 we
created a new window by adding a window to the existing main.nib
file.
Now we’ll create a new nib file and then put a window in it.
As you write more sophisticated and complex applications, you may find you need a large number of interface resources (document windows, tool palettes, message boxes, and so forth), but the resources don’t need to be available all the time. For example, in an editing application, a user may sometimes need one document window, but at other times needs multiple document windows along with several tool palettes.
Although you can define all interface resources in the main.nib
file,
it’s more memory efficient to create one or more auxiliary nib files.
A good strategy is to limit what you put in the main.nib
file
to the menu bar and perhaps one window—the minimum number of items
that must be open when your application launches. Then create auxiliary
nib files for other interface resources.
A document window, such as the itinerary window, is a perfect candidate for an auxiliary nib file. You’ll create a new nib file for a window that you’ll use to display the contents of the itinerary file your users open:
Double-click
the main.nib
file in
the Moon Travel Planner project to activate Interface Builder. (If
you are already in Interface Builder, you don’t need to do this.)
Choose New from the File menu.
Select a Carbon window from the Starting Points dialog, as shown in Figure 11.12. When you click New, an untitled nib file opens along with a new window.
Choose Save As from the File menu. Name the nib file itinerary.nib
.
You should save the nib file to the English.lproj
folder.
Name the window object Itinerary
,
as shown in Figure 11.13. In the Instances pane of the itinerary.nib
window,
double-click the word “Window,” type Itinerary
,
and press Return. This is the name by which you’ll
refer to the window in your code. You’ll use this name to create
the window from the nib file.
Use the Show Info window to enter Itinerary
as
the window’s title. The title is the text users see in the
title bar at the top of the window. With the window
active, choose Show Info from the Tools menu, choose Attributes from
the pop-up menu at the top of the Info window, type Itinerary
in
the Title text field, and press Return.
Choose Document from the Window Class pop-up menu.
Set the window’s controls. In the Controls group, make sure Close Box and Collapse Box are the only checkboxes selected.
Set the window’s attributes. In the Attributes group, make sure Standard Handler and Resizable are selected.
Resize the window to 500 by 600 pixels. Choose Size from
the pop-up menu at the top of the Show Info window, choose Width/Height
from the right Content Rect pop-up menu, and type 500
for width
and 600
for height. Then press Return.
Choose Save from the File menu. You’re done with the
window. Now you need to add the itinerary.nib
file
to your project.
Click the Project Builder icon in the Dock to make it active.
Choose Add Files from the Project menu and select the itinerary.nib
file.
When the dialog shown in Figure 11.14 opens, click Open, select “Copy into group’s folder,” and click Add.
If necessary, in the project window, drag the itinerary.nib
file
reference to the Resources group.
Before you begin adding the code to your Moon Travel Planner
application to open and save itinerary files, you need to define the
constants and global variables
that you will need later. Copy the code shown in Example 11.1 to the main.c
file,
after the kPrintInfoPtrProperty
constant
that you defined in Chapter 9.
Example 11-1. Constants for Opening and Saving Itinerary Files
#define kMTPOpenItineraryCommand 'oPit' #define kMTPSaveAsItineraryCommand 'sAit' #define kMTPDocType 'iTin' #define kMTPCFStringProperty 'cfst'
Here’s what the constants represent:
kMTPOpenItineraryCommand
(the
Open command). In Chapter 7
you defined the commands for opening and saving itineraries in Interface
Builder. To make it easier to refer to the Open and Save commands,
you should define constants for the four-character codes that represent
the commands.
kMTPSaveAsItineraryCommand
(the
Save As command). Just as with the Open command, it is easier to
refer to the four-character code for the Save As command if we define
a constant for it.
kMTPDocType
. The
file type of the documents that the Moon Travel Planner application
creates. You will learn more about file types and how to provide
them in Chapter 13.
kMTPCFStringProperty
. A
constant that you will use to store and retrieve information about
the itinerary text, as a “property” of the itinerary window.
You stored printing information in a similar manner in Chapter 9.
Finally, let’s define a global variable. Paste the following
line into the main.c
file
after the declaration for MTPDocumentPrintInfoPtr
:
NavEventUPP gEventProc;
You will use this variable to hold a pointer to the navigation event handler that you will define in Section 11.2.4.
You’ve already created the definition for the itinerary
window’s appearance using Interface Builder; now you need to implement
the itinerary window’s behavior. For the itinerary window, we
will define an event handler for the window events (events of class kEventClassWindow
)
and command events (events of class kEventClassCommand
)
that we wish to handle. Back in Chapter 7 when you created
the Moon Travel Planner application’s menu bar, you selected the
menu commands for opening, saving, and closing an itinerary, and
you assigned them four-character code constants to identify them.
In this section, you will write the MTPItineraryWindowEventHandler
,
which responds to those commands.
First, let’s declare the itinerary window event handler.
Copy the following lines into the main.c
file,
after the declaration for the MTPDoPrintLoop
function:
OSStatus MTPItineraryWindowEventHandler (EventHandlerCallRef handlerRef, EventRef event, void *userData);
Now implement it. Copy the MTPItineraryWindowEventHandler
function
in Example 11.2 into
the main.c
file, after
the body of the MTPDoPrintLoop
function:
Example 11-2. The Event Handler for the Itinerary Window
OSStatus MTPItineraryWindowEventHandler (EventHandlerCallRef handlerRef, EventRef event, void *userData) { OSStatus err; HICommandcommand; UInt32 eventKind; UInt32 eventClass; eventClass = GetEventClass(event); //1 eventKind = GetEventKind (event); //2 err = eventNotHandledErr; //3 if ((eventClass == kEventClassWindow) && (eventKind == kEventWindowClose)) { SelectWindow (gMainWindow); //4 err = MTPDoCloseItinerary((WindowRef)userData); //5 } else if (eventClass == kEventClassCommand) { GetEventParameter (event, kEventParamDirectObject, typeHICommand, NULL, sizeof (HICommand), NULL, &command); //6 switch (command.commandID) { case kMTPOpenItineraryCommand: err = MTPDoOpenItinerary((WindowRef)userData); //7 break; case kMTPSaveAsItineraryCommand: err = MTPDoSaveItineraryAs((WindowRef)userData); //8 break; case kMTPCloseCommand: SelectWindow (gMainWindow); err = MTPDoCloseItinerary((WindowRef)userData); //9 break; } } return err; }
Here is what the itinerary window command event handler does:
The Carbon
Event Manager function GetEventClass
returns
the class of the event that triggered the call to the itinerary
window event handler.
The Carbon Event Manager function GetEventKind
returns
the particular type of event that triggered the call to the itinerary
window event handler.
The event handler initializes the error message to eventNotHandledErr
.
If the handler does not handle the event that was passed to it,
the Carbon Event Manager will handle the event if it can.
If the event was a window close event—that is, if the user
clicked the itinerary window’s close button—the Window Manager
function SelectWindow
makes
the Moon Travel Planner main window the active window.
If the event was a window close event, the MTPDoCloseItinerary
function,
which you will write in Section 11.2.6 closes
the itinerary window.
If the event was a command event, the Carbon Event Manager
function GetEventParameter
retrieves
the command that triggered the call to the handler.
If the command was the Open command, the handler calls the
function MTPDoOpenItinerary
to
open an itinerary from an existing file. You will write the MTPDoOpenItinerary
function
in Section 11.2.4. The MTPDoOpenItinerary
function,
along with the other Moon Travel Planner functions called by the
event handler, takes a window reference (WindowRef
)
as its argument. When you install the window event handler in Section 11.2.4.4 you will specify a pointer
to the itinerary window as an argument to the InstallWindowEventHandler
function.
The Carbon Event Manager passes this reference back to the event
handler in the userData
parameter
when it calls the handler.
If the command was the Save As command, the handler calls
the function MTPDoSaveItineraryAs
,
which displays a Save dialog that allows the user to specify a new
name and location for the file, and then saves the file. You will
write this function in Section 11.2.5.
If the command was the Close command, the handler makes the
same function calls as it did for the window close event in steps
4 and 5. It first makes the Moon Travel Planner main window the
active window by calling SelectWindow
,
and then calls the MTPDoCloseItinerary
function
to close the window.
When a user selects the Open command from the File menu in the Moon Travel Planner application, the application needs to provide an interface for the user to select the file to open and then to open the itinerary file chosen by the user. To open a previously created itinerary file, you need to do six things:
Configure and display an Open dialog that lets the user locate the file to open.
Respond to the user’s action.
Retrieve the file to open.
Create a new itinerary window in which to display the contents of the file.
Read the data in from the itinerary file.
Display the contents of the itinerary file in the itinerary window.
The first step in opening the itinerary file is to create
and display a dialog that allows the user to choose the itinerary
file to open. In Section 11.2.3
you wrote your itinerary window event handler to respond to the
Open Itinerary command by calling the MTPDoOpenItinerary
function.
Now, you will implement that function.
First, let’s declare the function. Copy the following lines
into the main.c
file,
after the declaration for the MTPItineraryWindowEventHandler
function:
OSStatus MTPDoOpenItinerary();
Now implement the function. Copy the MTPDoOpenItinerary
function
in Example 11.3 into the main.c
file.
Example 11-3. A Function that Creates and Displays an Open Dialog
OSStatus MTPDoOpenItinerary() { OSStatus err = noErr; NavDialogRef theOpenDialog; NavDialogCreationOptions dialogOptions; if (( err = NavGetDefaultDialogCreationOptions( &dialogOptions)) == noErr ) { //1 dialogOptions.modality = kWindowModalityAppModal; //2 gEventProc = NewNavEventUPP( MTPNavEventCallback ); //3 if ((err = NavCreateGetFileDialog( &dialogOptions, NULL, gEventProc, NULL, NULL, NULL, &theOpenDialog )) == noErr) { //4 if ( theOpenDialog != NULL ) { if (( err = NavDialogRun( theOpenDialog )) != noErr) { //5 NavDialogDispose( theOpenDialog ); //6 DisposeNavEventUPP( gEventProc ); } } } } return err; }
Here’s what the MTPDoOpenItinerary
function
does:
The Navigation
Services function NavGetDefaultDialogCreationOptions
fills
out a NavDialogCreationOptions
structure
with default values for configuring the Open dialog. The dialog
options structure tells Navigation Services how to set up such features
as the size, location, and modality of the Open dialog.
The function specifies that the dialog should be application
modal by setting the modality
field
of the NavDialogCreationOptions
structure
to kWindowModalityAppModal
. An
application modal dialog takes over the application until the user
responds to the dialog. For more information on modality, see Inside
Mac OS X: Aqua Human Interface Guidelines.
The Navigation Services function NewNavEventUPP
creates
a pointer to the Moon Travel Planner navigation event handler, MTPNavEventCallback
.
When the user dismisses the Open dialog, Navigation Services calls
the navigation event handler with information about the event that
triggered the call. You will write the navigation event handler
in Section 11.2.4.2.
The Navigation Services function NavCreateGetFileDialog
creates
the Open dialog, with the characteristics specified by the NavDialogCreationOptions
structure. Passing the pointer to the navigation
event handler in the third
parameter tells Navigation Services which callback function to call
when the user takes an action.
The Navigation Services function NavDialogRun
displays
the Open dialog to the user. If there is no error in displaying
the dialog, the navigation event handler performs the next step
in opening the itinerary file.
If there was an error displaying the Open dialog, the Navigation
Services function NavDialogDispose
disposes
of the dialog reference, and the Navigation Services function DisposeNavEventUPP
disposes
of the pointer to the navigation event callback.
In the previous section, you implemented the MTPDoOpenItinerary
function,
which creates the dialog that allows the user to select which file
to open. When displaying a dialog using Navigation Services, the NavDialogRun
function
may return before the user has responded to the dialog. How, then,
do you know when the user has taken an action? Furthermore, how
do you know what that action is, once it has been taken? The answer:
through a navigation event handler.
As we discussed in Section 11.1.2.1 Navigation Services calls your navigation event handler when a navigation event occurs. For the most part, the type of event that you will be interested in is a user action event, although there are other types of events that you can choose to handle. However, the Moon Travel Planner application only has to handle the user action event. When you receive the user action event, you can retrieve the user’s response to the dialog and perform whatever operations are appropriate for the user’s action.
In this section, you’ll write the MTPNavEventCallback
function
and handle the user action for opening a file. When you created
the Open dialog, you specified the navigation event handler, MTPNavEventCallback
.
When the user responds to the Open dialog, by clicking either the
Cancel or Open button, Navigation Services calls MTPNavEventCallback
with information
about the user’s response. You can then proceed with opening the
file, if the user has indeed selected a file to open.
First, let’s declare the navigation event handler. Copy
the following lines into the main.c
file,
after the declaration for the MTPDoOpenItinerary
function:
pascal void MTPNavEventCallback( NavEventCallbackMessage callBackSelector, NavCBRecPtr callBackParms, void* callBackUD);
To implement the MTPNavEventCallback
function,
copy the MTPNavEventCallback
function
in Example 11.4 into
the main.c
file.
Example 11-4. The Navigation Event Handler
pascal void MTPNavEventCallback( NavEventCallbackMessage callBackSelector, NavCBRecPtr callBackParms, void* callBackUD) { OSStatus err = noErr; switch (callBackSelector) { case kNavCBUserAction: { //1 NavReplyRecord reply; NavUserAction userAction = 0; if ((err = NavDialogGetReply (callBackParms->context, &reply )) == noErr ) { //2 userAction = NavDialogGetUserAction ( callBackParms->context); //3 switch (userAction) { case kNavUserActionOpen: { MTPOpenTheFile (&reply); //4 break; } } err = NavDisposeReply (&reply); //5 } break; } case kNavCBTerminate: { //6 NavDialogDispose (callBackParms->context); //7 DisposeNavEventUPP (gEventProc); //8 break; } } }
The callback selector passed as an argument to your navigation event handler tells it what type of event triggered the call. Here’s what the navigation event handler does:
The function tests for a user action event. If the event was a user action, you should handle this event, because information about the user’s response to the dialog is described by this type of event.
The Navigation Services function NavGetDialogReply
retrieves
information about the user’s response in the form of a NavReplyRecord
structure.
If the dialog is an Open dialog, the reply record should hold the
information necessary to identify the file to open. If it was a
Save dialog, the reply record would contain the information necessary to
identify the location where the file should be saved and the name
that the file should be given.
The Navigation Services function NavDialogGetUserAction
determines
exactly what type of action the user took. The user action that
you should respond to is the action represented by the kNavUserActionOpen
constant.
If this is the action returned by Navigation Services, then you
can go ahead and open the file.
If the user chose a file to open (that is, if the last step
returned the kNavUserActionOpen
constant),
then the callback function calls the MTPOpenTheFile
function
to finish opening the file.
The Navigation Services function NavDisposeReply
disposes
of the reply record after the function MTPOpenTheFile
has
exited.
If the event that triggered the call to the callback function
was kNavCBTerminate
,
then the dialog has been terminated, and the callback should tidy
up.
The function NavDisposeDialog
disposes
of the open dialog.
The function DisposeNavEventUPP
releases
the memory associated with the pointer to the navigation event callback
function.
In the last section, you wrote the navigation event handler,
which retrieves the user’s reply from Navigation Services. If
the user has indeed selected a file to open, the handler calls the MTPOpenTheFile
function
to finish opening the itinerary file. In this section, you will implement
the MTPOpenTheFile
function.
Declare the MTPOpenTheFile
function.
Copy the following lines into the main.c
file,
after the declaration for the MTPNavEventCallback
function:
void MTPOpenTheFile(NavReplyRecord *reply);
Now implement the function. Copy the MTPOpenTheFile
function
in Example 11.5 into
the main.c
file.
Example 11-5. A Function that Retrieves the File to Open
void MTPOpenTheFile(NavReplyRecord *reply) { AEDesc actualDesc; FSRef fileToOpen; HFSUniStr255 theFileName; CFStringRef fileNameCFString; WindowRef newWindow; OSStatus err; if ((err = AECoerceDesc(&reply->selection, typeFSRef, &actualDesc)) == noErr) //1 { if (&actualDesc != NULL) { if ((err = AEGetDescData (&actualDesc, (void *)(&fileToOpen), sizeof(FSRef)) == noErr)) //2 { err = FSGetCatalogInfo ( &fileToOpen, kFSCatInfoNone, NULL, &theFileName, NULL, NULL ); //3 fileNameCFString = CFStringCreateWithCharacters (NULL, theFileName.unicode, theFileName.length ); //4 newWindow = MTPDoNewItinerary( fileNameCFString); //5 ShowWindow(newWindow); //6 err = MTPReadFile(&fileToOpen, newWindow); //7 } } AEDisposeDesc(&actualDesc); //8 } }
Here’s what the MTPOpenTheFile
function
does:
The Apple
Event Manager function AECoersceDesc
coerces
the data returned by Navigation Services in the NavReplyRecord
structure
to validate that it is indeed an FSRef
.
If the coercion succeeds, the Apple Event Manager function AEGetDescData
retrieves
the FSRef
identifying
the file to open.
The File Manager function FSGetCatalogInfo
returns
the name of the file. You will use the filename to set the title
of the itinerary window. The FSCatalogInfo
call
can return much more information about the file, such as its creation
date, size, permissions, and so forth. However, all you need for
the Moon Travel Planner is the name, so you specify kFSCatInfoNone
to
indicate that you do not wish to retrieve any additional information.
The Core Foundation String Services function CFStringCreateWithCharacters
converts the
Unicode filename returned by the File Manager into a CFString
representation.
The File Manager uses the HFSUniStr255
data
type to store Unicode filenames; the function that you will use
in the next section to set the window title expects a CFStringRef
data type.
The MTPDoNewItinerary
function
creates a window in which to display the itinerary and returns that
window. You will write the MTPDoNewItineray
function
in the next section.
The Window Manager function ShowWindow
shows
the itinerary window, which is created to be initially hidden.
The MTPReadFile
function
reads in the itinerary data from the file. You will write the MTPReadFile
function
in Section 11.2.4.5.
The Apple Event Manager function AEDisposeDesc
disposes
of the Apple Event descriptor that you used to retrieve the FSRef
for
the file to open.
The last function that you wrote, MTPOpenTheFile
,
gets all of the information that you need in order to identify the
file to open. You’re still not ready to read the itinerary data
from the file, however. Before you can do that, you need to create
the itinerary window in which you will display the information that
you read from the itinerary file. It might seem as if you’ve already
done just that. However, in Section 11.2.1
you defined only the appearance and characteristics of the itinerary
window. In this section, you’ll create an actual instance of an
itinerary window to hold the itinerary text. The MTPDoNewItinerary
function
creates and initializes a new itinerary window.
First, declare the MTPDoNewItinerary
function.
Copy the following lines into the main.c
file, after
the declaration for the MTPOpenTheFile
function:
WindowRef MTPDoNewItinerary(CFStringRef inName);
Now implement the function. Copy the MTPDoNewItinerary
function
in Example 11.6 into
the main.c
file.
Example 11-6. A function that creates a new itinerary window
WindowRef MTPDoNewItinerary(CFStringRef inName) { IBNibRef itineraryNib; EventTypeSpec itinerarySpec[2] = { {kEventClassCommand,kEventCommandProcess}, {kEventClassWindow, kEventWindowClose}}; //1 OSStatus err; WindowRef theWindow; err = CreateNibReference(CFSTR("itinerary"), &itineraryNib); //2 require_noerr( err, CantGetNibRef ); err = CreateWindowFromNib(itineraryNib, CFSTR("Itinerary"), &theWindow); //3 require_noerr( err, CantCreateWindow ); DisposeNibReference(itineraryNib); //4 err = InstallWindowEventHandler (theWindow, NewEventHandlerUPP (MTPItineraryWindowEventHandler), 2, itinerarySpec, (void *) theWindow, NULL); //5 err = SetWindowTitleWithCFString ( theWindow, inName); //6 return theWindow; //7 CantCreateWindow: CantGetNibRef: return NULL; }
Here’s what the MTPDoNewItinerary
function
does:
MTPDoNewItinerary
initializes
an event specification, identifying the events that the itinerary
window handles. You use this specification to register the itinerary’s
window event handler in step 5. The itinerary window handles two
types of events: command events and window close events.
The function CreateNibReference
finds
and opens the itinerary.nib
file
that you created in Section 11.2.1.
The function CreateWindowFromNib
creates
the itinerary window, using the definition in the itinerary.nib
file.
The function DisposeNibReference
disposes
of the nib reference, which we no longer need now that the window
has been created.
The Carbon Event Manager function InstallWindowEventHandler
registers
the itinerary event handler that you wrote in Section 11.2.3.
You pass a reference to the newly created itinerary window to the InstallWindowEventHandler
function.
When the Carbon Event Manager calls MTPItineraryWindowEventHandler
,
the Carbon Event Manager passes the reference to the itinerary window
back to your application.
The Window Manager function SetWindowTitleWithCFString
sets
the title of the itinerary window to the name of the file, which
is passed as an argument in the inName
parameter.
The MTPDoNewItinerary
function
returns a reference to the new window that it creates.
Finally, now that you have an itinerary window in which to
display the itinerary text, you are ready to read the data from
the itinerary file on disk. The MTPReadFile
function
opens the itinerary file and reads data from the itinerary’s data
fork.
First, declare the MTPReadFile
function.
Copy the following lines into the main.c
file,
after the declaration for the MTPDoNewItinerary
function:
OSStatus MTPReadFile(FSRef *inFSRef, WindowRef theWindow);
Now implement the function. Copy the MTPReadFile
function
in Example 11.7 into
the main.c
file.
Example 11-7. A Function that Reads Data from an Itinerary File
OSStatus MTPReadFile(FSRef *inFSRef, WindowRef theWindow) { ByteCount count, actualCount; SInt16 forkRefNum=0; UniChar buffer[256]; OSStatus err = noErr; CFMutableStringRef theText; HFSUniStr255 forkName; theText = CFStringCreateMutable (NULL, 0); //1 err = FSGetDataForkName (&forkName); //2 err = FSOpenFork (inFSRef, forkName.length, forkName.unicode, fsRdPerm, &forkRefNum); //3 if (err == noErr) { do { count = 256 * sizeof(UniChar); //4 err = FSReadFork (forkRefNum, fsAtMark, 0, count, &buffer, &actualCount); //5 actualCount/= sizeof (UniChar); //6 CFStringAppendCharacters (theText, buffer, actualCount); //7 } while (err == noErr); if (err == eofErr) { //8 err = noErr; MTPDisplayFile (theText, theWindow); //9 err = SetWindowProperty (theWindow, kMTPApplicationSignature, kMTPCFStringProperty, sizeof (CFStringRef), (void *)&theText); //10 } err = FSCloseFork(forkRefNum); //11 } return err; }
Here’s what the MTPReadFile
function
does:
The Core
Foundation String Services function CFStringCreateMutable
creates
a new, changeable, CFString
object. You will use this CFString
object
to hold the text that you read from the itinerary file.
The File Manager function FSGetDataForkName
returns
the name of the data fork of the file where the itinerary text
is stored.
The File Manager function FSOpenFork
opens
the data fork of the itinerary file. We request read permission
by specifying the fsRdPerm
constant.
The FSOpenFork
function returns
a fork reference number that identifies the access path to the itinerary
file that the File Manager has created. You will use this fork reference
number to read the file.
At the beginning of the do
loop,
the MTPReadFile
function
initializes the count variable to the size of 256 Unicode characters,
the size of the buffer that it has allocated to receive the data
read in by the File Manager.
While data still remains to be read from the file, the File
Manager function FSReadFork
reads
data from the file into the buffer that you have provided. Specifying fsAtMark
to the FSReadFork
function
indicates that the read operation should begin at the current position
of the file mark. Since the file mark gets moved with each read
we make, on the next call to FSReadFork
,
we pick up right where we left off. When you opened the file
with the FSOpenFork
function,
the File Manager automatically set the file mark to the start of
the file. Therefore, when you read the file, you automatically start
reading at the beginning. If you were accessing the file via an
access path that had not just been opened or if you wished to start
reading from elsewhere in the file, you would call the File Manager
function FSSetForkPosition
to
ensure that you began reading the file from the intended position.
The MTPReadFile
function
divides the number of total bytes read by the size of a Unicode
character to get the number of Unicode characters in the buffer.
The Core Foundation String Services function CFStringAppendCharacters
appends
the text that the File Manager read from the file to the CFString
object.
Normally, you would not read in the
data from the file and store it in a string. Instead, you would
typically associate it with some sort of document record that contains
the information that your application needs to create, track, and manipulate
a document in your application. In this particular case, the itinerary document
would be a text editing document, using Multilingual Text Engine
(MLTE), to handle text editing. You would then associate
the file and the file data with the “text object” that represents
a simple MLTE document. However, text editing is beyond the scope
of this tutorial, and we use the CFString
object simply to allow
us to illustrate the tasks of saving data to a file and retrieving
data from a file.
The MTPReadFile
function
tests for the end-of-file error. When the FSReadFork
function returns
an end-of-file error (represented by the eofErr
constant),
all of the data has been read from the file and the loop terminates.
If the loop terminates because of another error, the MTPReadFile
function
exits.
The MTPDisplayFile
function
displays the text that you’ve read in from the file in the itinerary
window. You will write MTPDisplayFile
next in
Section 11.2.4.6.
The Window Manager function SetWindowProperty
associates
the CFString
holding the itinerary text with the itinerary window.
The File Manager function FSCloseFork
closes
the data fork of the itinerary file.
Your last task in opening a file is to display the contents
of the file that you’ve just read. To do this, you will write
the MTPDisplayFile
function.
First, declare the MTPDisplayFile
function.
Copy the following line into the main.c
file, after
the declaration for the MTPReadFile
function:
void MTPDisplayFile(CFStringRef stringToDisplay, WindowRef inWindow);
Now implement the function. Copy the MTPDisplayFile
function
in Example 11.8 into
the main.c
file.
Example 11-8. A Function that Displays the Contents of an Itinerary File
void MTPDisplayFile (CFStringRef stringToDisplay, WindowRef inWindow) { Rect bounds; GrafPtr oldPort; GetPort (&oldPort); //1 SetPortWindowPort (inWindow); //2 EraseRect (GetWindowPortBounds (inWindow, &bounds)); //3 TXNDrawCFStringTextBox (stringToDisplay, &bounds, NULL, NULL); //4 SetPort (oldPort); //5 }
Here’s what the MTPDisplayFile
function
does:
The QuickDraw
function GetPort
gets
the current graphics port; you will use this later on to restore
the graphics port.
The Window Manager function SetPortWindowPort
sets
the graphics port to the itinerary window’s port.
The QuickDraw function EraseRect
makes
sure that the area bounded by the window is empty. The Window Manager
function GetWindowPortBounds
returns
the boundaries of the itinerary window, which the EraseRect
function
uses as the bounds of the area that it erases.
The Multilingual Text Engine (MLTE) function TXNDrawCFStringTextBox
draws
the contents of the CFString
in which we’ve stored the itinerary
text into the window. This is static text. Note that if you were
displaying the contents of a real user file, you should allow the
user to view and change the document, so you would have to display
the text differently. For editable text documents, MLTE does most
of the work of displaying the text onscreen for you. For more information,
see the Multilingual Text Engine documentation in Carbon Help.
The QuickDraw function SetPort
restores
the current graphics port to its original state.
A user who creates an itinerary for the
moon trip may wish to save it as a text file. When the user invokes
the Save or Save As command, either by selecting them in the File
menu or by using the Command-S
keyboard
shortcut, your application needs to save the contents of the itinerary
to disk. In this section, we will implement the Save As command,
to save a new file. To save a new file you must:
Configure and display a Save dialog that lets the user specify the name and location for the file to save.
Respond to the user’s action.
Create the Save file to hold the itinerary text.
Write the itinerary contents to the file.
Once you’ve implemented the Save As command
for new files, implementing the Save command is relatively simple.
When a user selects the Save command from the File menu, or types
Command-S
, your application should check to see if a file
exists for the current document. If not, it’s the same as a Save
As operation, and you should create a new file. If a file already
exists on disk for the document, you can skip right to step 4 in
the list above, writing the data to disk. Because we are not implementing
an editable itinerary window in this tutorial, there is little point
in implementing the Save command (there would be no changes to
save!). However, as described here, once you’ve implemented the Save
As command, adding in the handler for the Save command is not difficult.
When the user selects the Save As command or when the user tries to save an itinerary for which a file does not yet exist, you must provide an interface to the user that allows her to choose the location at which to store the file and the name that she wishes to give to the new file. Only then can you proceed with the save operation. In this section, you will write the code to display a Save dialog.
First, declare the MTPDoSaveItineraryAs
function.
Copy the following lines into the main.c
file,
after the declaration for the MTPDisplayFile
function:
OSStatus MTPDoSaveItineraryAs(WindowRef theItineraryWindow);
Now implement the function. Copy the MTPDoSaveItineraryAs
function
in Example 11.9 into the main.c
file.
Example 11-9. A Function that Creates and Displays a Save Dialog
OSStatus MTPDoSaveItineraryAs (WindowRef theItineraryWindow) { OSStatus err = noErr; NavDialogRef theSaveDialog; NavDialogCreationOptions dialogOptions; if (( err = NavGetDefaultDialogCreationOptions (&dialogOptions )) == noErr ) { //1 err = CopyWindowTitleAsCFString( theItineraryWindow, &dialogOptions.saveFileName ); //2 dialogOptions.parentWindow = theItineraryWindow; //3 dialogOptions.modality = kWindowModalityWindowModal; //4 gEventProc = NewNavEventUPP( MTPNavEventCallback ); //5 if ((err = NavCreatePutFileDialog(&dialogOptions, kMTPDocType, kMTPApplicationSignature, gEventProc, (void *)(theItineraryWindow), &theSaveDialog)) == noErr) { //6 if ( theSaveDialog != NULL ) { if (( err = NavDialogRun (theSaveDialog)) != noErr) { //7 NavDialogDispose( theSaveDialog ); //8 DisposeNavEventUPP( gEventProc ); //9 } } } if ( dialogOptions.saveFileName != NULL ) CFRelease( dialogOptions.saveFileName ); //10 } return err; }
Here’s what the MTPDoSaveItineraryAs
function
does:
The Navigation
Services function NavGetDefaultDialogCreationOptions
fills
out a NavDialogCreationOptions
structure
with standard default values. The dialog options structure tells
Navigation Services how to set up such features as the size, location,
and modality of the Save dialog.
The function CopyWindowTitleAsCFString
retrieves
the itinerary window’s title and assigns it to be the default
filename that appears in the Save dialog.
The MTPDoSaveItineraryAs
function
sets the itinerary window as the parent window of the Save dialog;
this makes the dialog a sheet belonging to the itinerary window.
In the Aqua user interface, sheets are dialogs
that belong to a particular window and appear to emerge from that
window to emphasize the connection between the dialog and the parent
window. Save dialogs should be sheets. The Save dialog is only relevant
to the window that received the Save command, and the direct correlation
between the window to be saved and the Save dialog is made clear
by the visual presentation of the dialog.
The MTPDoSaveItineraryAs
function
sets the modality of the dialog creation options to kWindowModalityWindowModal
to
make the dialog a sheet. A sheet is a window modal dialog, meaning
it prevents all further action upon the window until the user addresses the
dialog. The sheet does not, however, interfere with other documents
within the same application. Window modal is not the default value
in the dialog creation options structure, so you must specify the
modality before creating the dialog.
The Navigation Services function NewNavEventUPP
allocates
a pointer to the navigation event handler that you wrote in Section 11.2.4.2. In the next section, you will
add code to this handler to handle saving. This navigation event handler
is called by Navigation Services when the user responds to the Save
dialog.
The Navigation Services function NavCreatePutFileDialog
creates
the Save dialog. Passing the pointer to the navigation event handler
tells Navigation Services to call this function when the user responds
to the dialog. When calling NavCreatePutFileDialog
,
pass a pointer to the itinerary window in the callBackUD
parameter.
When the dialog is dismissed, Navigation Services passes this window
reference back to your navigation event handler, so that you may
use it in further calls.
The Navigation Services function NavDialogRun
displays
the Save dialog.
If there was an error displaying the dialog, the NavDisposeDialog
function
disposes of the Save dialog.
Also in case of an error displaying the dialog, the DisposeNavEventUPP
function
disposes of the pointer to the save event callback function.
When the NavDialogRun
function
returns, the MTPDoSaveItineraryAs
function
checks for a valid CFString
in the saveFileName
field
of the dialog creation options structure and, if it is valid, releases
the CFString
.
In the previous section, you created and displayed a Save dialog to allow the user to specify the name and location of the new file. Just as in Section 11.2.4 you do not know when the user has responded to the dialog, or what that response is, until Navigation Services calls your navigation event handler. In this section, you will add a few lines of code to the event handler that you wrote in the section “Open an Itinerary File,” so that the navigation event handler responds to save as well as open events.
Modify the second switch statement in the MTPNavEventCallback
function
so it now looks like the code shown in Example 11.10.
Example 11-10. Navigation Event Handler Switch Statement After Adding “Save As” Code
switch (userAction) { case kNavUserActionOpen: { MTPOpenTheFile (&reply); break; } case kNavUserActionSaveAs: { err = MTPDoFSRefSave ((WindowRef)(callBackUD), &reply ); break; } }
The navigation event handler works exactly as it did before,
except now it handles saving in addition to opening. The new lines
of code state that if the user action that triggers the call to
the navigation event handler is a Save As action, the navigation
event handler calls the Moon Travel Planner function MTPDoFSRefSave
to
save the file. You will write the MTPDoFSRefSave
function
in the next section.
You have now created and displayed a dialog for the save operation
and retrieved the user response. Your navigation event handler,
which you created in Section 11.2.5.2,
called the function MTPDoFSRefSave
to
continue the save operation. The MTPDoFSRefSave
function
creates the new file from the information that your navigation event
handler retrieved from Navigation Services and then prepares the
file for saving.
Normally, if you are implementing the Save As command, you would open the newly created file and display it in the user’s current document window. To keep things simple in this book, we’ve chosen to omit that part.
First, declare the MTPDoFSRefSave
function.
Copy the following line into the main.c
file, after
the declaration for the MTPDoSaveItineraryAs
function:
OSErr MTPDoFSRefSave (WindowRef theItineraryWindow, NavReplyRecord* reply );
Now implement the function. Copy the MTPDoFSRefSave
function
in Example 11.11 into
the main.c
file.
Example 11-11. A Function that Creates a Save File
OSErr MTPDoFSRefSave (WindowRef theItineraryWindow, NavReplyRecord* reply ) { OSErr err = noErr; FSRef fileRefParent; AEDesc actualDesc; if ((err = AECoerceDesc (&reply->selection, typeFSRef, &actualDesc )) == noErr) { //1 if ((err = AEGetDescData( &actualDesc, (void *)&fileRefParent, sizeof( FSRef ) )) == noErr ) { //2 UniChar* nameBuffer = NULL; UniCharCount sourceLength = 0; sourceLength = (UniCharCount) CFStringGetLength( reply->saveFileName ); //3 nameBuffer = (UniChar*)NewPtr (sourceLength *2); //4 CFStringGetCharacters ( reply->saveFileName, CFRangeMake( 0, sourceLength), &nameBuffer[0] ); //5 if (nameBuffer != NULL) { if ( reply->replacing ) { FSRef fileToDelete; if ((err = FSMakeFSRefUnicode (&fileRefParent, sourceLength, nameBuffer, kTextEncodingUnicodeDefault, &fileToDelete )) == noErr) { //6 err = FSDeleteObject( &fileToDelete ); //7 } } if ( err == noErr ) { FSRef newFSRef; FileInfo *fileInfo; FSCatalogInfo catalogInfo; fileInfo = (FileInfo *) &catalogInfo.finderInfo[0]; //8 BlockZero(fileInfo, sizeof(FileInfo)); //9 fileInfo->fileType = kMTPDocType; //10 fileInfo->fileCreator = kMTPApplicationSignature; //11 if ((err = FSCreateFileUnicode (&fileRefParent, sourceLength, nameBuffer, kFSCatInfoFinderInfo, &catalogInfo, &newFSRef, NULL)) == noErr) { //12 MTPWriteFile (&newFSRef, theItineraryWindow); //13 } } } DisposePtr ((Ptr)nameBuffer ); //14 } AEDisposeDesc( &actualDesc ); //15 } return err; }
Here’s what the MTPDoFSRefSave
function
does:
The Apple
Event Manager function AECoerceDesc
checks
for a valid FSRef
by
coercing the Apple Event descriptor returned by Navigation Services
in the reply record.
If the coercion succeeds, the Apple Event Manager function AEGetDescData
extracts
the FSRef
from the descriptor.
This FSRef
does not describe
the new file itself, because that file does not yet exist. Rather,
the FSRef
returned in
the reply record describes the parent of the new file; that is,
it describes the directory in which the new file will reside.
The Core Foundation String Services function CFStringGetLength
gets
the length of the CFString
returned
by Navigation Services as the filename of the new file.
The NewPtr
function
allocates storage space for a buffer into which the filename will
be read.
The Core Foundation String Services function CFStringGetCharacters
retrieves
the name specified by the user for the new file. The name is read
from the CFString
into
a buffer of Unicode characters, which is the representation that
the File Manager expects.
If the user has chosen to replace the existing file, the File
Manager function FSMakeFSRefUnicode
makes
an FSRef
for the existing
file. The user has the option of replacing an existing file. In
that case, the application should delete the old file before saving
the new one. FSMakeFSRefUnicode
identifies
the file to delete using the FSRef
of
the file’s parent directory and the filename that you’ve
retrieved.
If the user has chosen to replace an existing file, the File
Manager function FSDeleteObject
deletes
the old file.
The function sets the pointer to the FileInfo
structure
to the finderInfo
field
of the catalogInfo
structure.
The finderInfo
field
of the catalogInfo
structure
contains information that the Finder uses to identify the file.
When we create the file, we will pass this information to the FSCreateFileUnicode
function,
so that the file is created with the appropriate Finder information.
Casting the finderInfo
field
to a FileInfo
structure
makes it easier to fill out the finder information that we wish
to set.
The BlockZero
function
initializes all of the fields of the FileInfo
structure
to zero.
The function sets the file type of the new file to the Moon Travel Planner application’s file type.
The function sets the file creator of the new file to the Moon Travel Planner application signature.
The File Manager function FSCreateFileUnicode
creates
a new file for the itinerary. The file is specified by the name
and the identity of the parent directory. The File Manager returns
an FSRef
for the new
file. When you create the file, you pass to the FSCreateFileUnicode
function,
the FSCatalogInfo
structure
containing the information that identifies your file to the Finder.
You also pass the kFSCatInfoFinderInfo
constant
to notify the File Manager that it should use this information to
set the Finder information for the new file.
The MTPWriteFile
function
opens the file and saves the itinerary data. You will implement
this function in Section 11.2.5.4.
The DisposePtr
function
disposes of the storage allocated to the filename buffer.
The Apple Event Manager function AEDisposeDesc
disposes
of the event descriptor used to retrieve the FSRef
from
the NavReplyRecord
structure.
The final step in saving a file is writing the data to disk.
The MTPWriteFile
function
opens the file for writing and calls FSWriteFork
to
write the itinerary data to disk.
First, declare the MTPWriteFile
function.
Copy the following lines into the main.c
file,
after the declaration for the MTPDoFSRefSave
function:
OOSStatus MTPWriteFile (FSRef *inRef, WindowRef theItineraryWindow);
Now implement the function. Copy the MTPWriteFile
function
in Example 11.12 into
the main.c
file.
Example 11-12. A Function that Writes the Itinerary Data to Disk
OSStatus MTPWriteFile (FSRef *inRef, WindowRef theItineraryWindow) { HFSUniStr255 forkName; SInt16 forkRefNum; CFIndex length; CFStringRef theString; OSStatus err; UniChar * buffer; err = GetWindowProperty (theItineraryWindow, kMTPApplicationSignature, kMTPCFStringProperty, sizeof(CFStringRef), NULL, (void *)&theString); //1 length = CFStringGetLength(theString); //2 buffer = (UniChar*)NewPtr(length * sizeof(UniChar)); //3 CFStringGetCharacters (theString, CFRangeMake(0, length), buffer); //4 err = FSGetDataForkName (&forkName); //5 if (( err = FSOpenFork (inRef, (UniCharCount)forkName.length, forkName.unicode, fsRdWrPerm, &forkRefNum)) == noErr) { //6 err = FSWriteFork(forkRefNum, fsFromStart, 0, length, (void *) buffer, NULL); //7 err = FSCloseFork(forkRefNum); //8 } free(buffer); //9 return err; }
Here’s what the MTPWriteFile
function
does:
The Window
Manager function GetWindowProperty
retrieves
the CFString
that you associated with the itinerary file in Section 11.2.4.5.
The Core Foundation String Services function CFStringGetLength
returns
the length of the itinerary text contained in the CFString
.
The NewPtr
function
allocates a buffer for the characters to write to disk. In this
case, the buffer should be the length of the text in the CFString
object.
The Core Foundation String Services function CFStringGetCharacters
retrieves
the text from the CFString
object into the buffer that we have provided.
As we mentioned earlier, in Section 11.2.4.5, if you were to make the itinerary file user editable, you would normally have some sort of structure that represents the document as a whole (such as an MLTE text object). You would have to retrieve the data associated with that document before writing it to disk.
The File Manager function FSGetDataForkName
retrieves
the name of the file’s data fork. You should always store your
data in the data fork. Currently, the File Manager simply returns
a constant for the data fork name, but using the function provided
by the File Manager ensures that future changes in implementation
won’t affect your code.
The File Manager function FSOpenFork
opens
the data fork of the file to save. When you open a file fork, you
must specify the type of access you wish to have to the fork. In
this case, we pass the constant fsRdWrPerm
to
the File Manager, requesting read and write privileges. A fork reference
number specifying the path to the open fork is returned; a fork
can have more than one path open at a time, as long as their permissions
don’t conflict.
The File Manager function FSWriteFork
writes
the specified number of bytes from the buffer containing the itinerary
text to the disk.
The function frees the memory allocated to the buffer.
At this point, you have one more command and one more event to handle. These are the Close command and the window close event. Luckily, you can use the same function for both of them. You will write that function in this section.
When you close a document, you should perform any clean-up that may be necessary for your program. In our case, this clean-up is quite simple. However, if you were working with a full-fledged editable text document, there would be more to do. For instance, you would want to check for any unsaved changes in the document, and if there are any, you would give the user a chance to save those changes before closing the window, using the Save Changes dialog that you learned about in Section 11.1.2.4. You would close any fork reference that you may have been using; dispose of text objects, pointers to callback functions, or any other objects for which you’ve allocated memory; and perform any other clean-up tasks that need to be done.
First, declare the MTPDoCloseItinerary
function.
Copy the following lines into the main.c
file,
after the declaration for the MTPWriteFile
function:
OSStatus MTPDoCloseItinerary(WindowRef inWindow);
Now implement the function. Copy the MTPDoCloseItinerary
function
in Example 11.13 into the main.c
file.
Example 11-13. A Function that Closes an Itinerary Window
OSStatus MTPDoCloseItinerary(WindowRef inWindow) { CFStringRef theString; OSStatus err = noErr; err = GetWindowProperty (inWindow, kMTPApplicationSignature, kMTPCFStringProperty, sizeof(CFStringRef), NULL, (void *)&theString); //1 if (theString != NULL) CFRelease (theString); //2 DisposeWindow(inWindow); //3 return err; }
Here’s what the MTPDoCloseItinerary
function
does:
The Window
Manager function GetWindowProperty
gets
the CFString
that you associated with the itinerary window in the MTPReadFile
function.
If the string is not empty, the Core Foundation function CFRelease
releases
the memory allocated to the CFString
.
The Window Manager function DisposeWindow
disposes
of the window.
As a last step, you need to add code to the Moon Travel Planner
application to handle the Open Itinerary command. All you need to do to
handle the Open Itinerary
command from the Moon Travel Planner main window, is add
a case to the switch statement in the main window event handler
you wrote in Chapter 6.
Modify the switch statement in the MTPMainWindowEventHandler
function so
it now looks like the code shown in Example 11.14.
Example 11-14. The Modified Switch Statement in the Main Window Event Handler
switch (command.commandID) { case kMTPComputeCommand: ComputeCommandHandler ((WindowRef) userData); result = noErr; break; case kMTPShowMoonFactsCommand: MTPShowMoonFactsWindow(gMoonFactsWindow); result = noErr; break; case kMTPOpenAboutWindowCommand: MTPAboutWindowCommandHandler(gAboutWindow); result = noErr; break; case kMTPOpenItineraryCommand: result = MTPDoOpenItinerary(); break; }
Now, when the user chooses Open Itinerary from the File menu, the Moon Travel Planner main window will process the command and open an itinerary window.
Now do the following:
Click the Build button in the upper-left corner of the Moon Travel Planner project window.
Click the Run button in the upper-left corner of the project window.
Open an itinerary by using the Open menu command. Select the Open Itinerary command from the Moon Travel Planner File menu. The application will bring up an Open dialog, such as the one shown in Figure 11.7. Use the column view browser or the Where pop-up menu to navigate to the Itineraries folder that was provided with the tutorial. In the column view browser, select the itinerary file “Itinerary” and click the Open button in the Open dialog. You should see the window shown in Figure 11.15.
Save an itinerary. Select the Save As command from the Moon Travel Planner File menu. The application will bring up a Save Location dialog, similar to the one in Figure 11.10. In the Save as text box, enter the filename for the new file. Choose the location in which to save the file by navigating through the file system with the column view browser (which you can see by clicking the disclosure button to the right of the Where pop-up menu) or by selecting a folder in the Where pop-up menu.
Click the Save button. A new file should appear in the location that you specified, with the filename that you provided. Right now this file has a generic document icon; in Chapter 13 you will learn how to provide a document icon that will be displayed for the documents that you create with Moon Travel Planner. You can open the saved file to make sure the text was actually saved.