|
Volume Number: 19 (2003)
Issue Number: 8
Column Tag: Recipes
X Files Carbonara
Making Navigation Easier for the Impatient
by Richard Patterson
The Good ol' ways
Before I was coerced into being carbonized, I had a simple scrap of code I could grab and use whenever I needed my application to create and write a file.
long byteCounter; StandardFileReply reply; FSSpec asciFile; short asciFileNum; char *textData; OSErr err = noErr; StandardPutFile("\pSave Text Data as:", "\pFullLookupTable.txt", &reply); if(reply.sfGood) { asciFile = reply.sfFile; FSpDelete(&asciFile); // ignore any error caused if there is no such file err = FSpCreate(&asciFile,'XCEL','TEXT', -1); /* -1 = system script */ err = FSpOpenDF(&asciFile, fsCurPerm, &asciFileNum); if (err != noErr) return err; err = FSWrite(asciFileNum, &byteCounter, textData); if (err != noErr) return err; err = FSClose(asciFileNum); } return err;
It couldn't get much more straightforward than that. Those were the days when the Toolbox really made life simpler for a programmer. It helped me tell the user (generally myself) what he was supposed to do and even suggest a default name for the file. The only suspect part of this code was the shortcut method of insuring that the file was indeed a virgin file by attempting to delete it before (re)creating it. I knew there were more elegant ways to let StandardPutFile tell me that the user was replacing an existing file rather than just creating a brand new one, but most of the time I didn't care. I just wanted to write the file and get on with it. (Most of my programming is for in-house use only, so I can get away with quick-and-dirty solutions that you should only try at home.)
Recently I was desperately trying to debug an After Effects plug-in to send images to a film recorder, and I realized the only way I was going to be able tell what was going on would be to save a file capturing the state of the image at a certain point. I don't write applications that write files all that often, but years ago I acquired the prejudice that reading and writing files is one of the most basic functions the operating system needs to do and should therefore be a very simple programming task. So I resurrected a scrap of code designed to save an image buffer as a simple Photoshop file and threw it into the soup.
The next thing I knew I was spending two days trying to figure out how to replace the method shown above with something that would work inside my carbon code running on OS-X. I found myself floating around in all kinds of convoluted discussions of Apple Events and Unicode text. I eventually vented my frustration on Apple Developer Support whose suggestions for further reading and examples only seemed to complicate what I thought I was beginning to grasp. Fortunately the support technician was very patient, and I was able to crystallize my ferment into a rational suggestion that OS-X should provide a much simpler higher level function to help the less experienced programmer create and write a file. The support technician agreed that was a good idea, but indicated that it was not at the top of their priorites. When I finally saw the light thanks to Mr. K.J. Bricknell's indispensable tome, Carbon Programming; I realized that perhaps I should write a sample function that might spare someone else the agony I had just experienced.
Non Standard
The functions for opening and writing to a file (FspOpenDF and FSWrite) still work in Carbon on OS-X, but the StandardPutFile and StandardGetFile functions have been replaced by Navigation Services. The system had just outgrown the functionality provided by the Standard File Package. There is a Navigation Services function NavPutFile that was the original replacement for StandardPutFile, but with OS-X Apple recommends that we use NavCreatePutFileDialog. If I had been keeping up, the transition from StandardPutFile to NavPutFile to NavCreatePutFileDialog might have been smooth and effortless. Instead I woke up and found the following definition staring me in the face:
OSStatus NavCreatePutFileDialog ( const NavDialogCreationOptions * inOptions, OSType inFileType, OSType inFileCreator, NavEventUPP inEventProc, void * inClientData, NavDialogRef * outDialog );
Then I discovered there were 11 other functions I must call before I can have an FSSpec to use in the familiar way.
What I wanted was one function that took care of all the user interaction and just gave me a ready-to-wear FSSpec. It would need to know what kind of file I am trying to create, so there are three things I need to give it: the file type, the file creator and a pointer to the FSSpec.
OSErr SimpleNavPutFile( OSType fileType, OSType fileCreator, FSSpec *theFileSpec) { OSStatus theStatus; NavDialogRef theDialog; NavReplyRecord theReply; AEDesc aeDesc; FSRef fsRefParent, fsRefDelete; UniChar *nameBuffer; UniCharCount nameLength; FInfo fileInfo; OSErr err = noErr; theStatus = NavCreatePutFileDialog(NULL, fileType, fileCreator, NULL, NULL, &theDialog); NavDialogRun(theDialog); theStatus = NavDialogGetReply ( theDialog, &theReply); NavDialogDispose(theDialog); if(!theReply.validRecord) { // Assuming the user changed his/her mind? No harm; no foul. // Still need to indicate that a file has not been created return -1; } err = AECoerceDesc(&theReply.selection, typeFSRef, &aeDesc); if(err != noErr) return err; err = AEGetDescData(&aeDesc, &fsRefParent, sizeof(FSRef)); if(err != noErr) return err; nameLength = (UniCharCount)CFStringGetLength(theReply.saveFileName); nameBuffer = (UniChar *) NewPtr((long)nameLength); CFStringGetCharacters(theReply.saveFileName, CFRangeMake(0, (long)nameLength), &nameBuffer[0]); if(nameBuffer == NULL) return -1; // generic error if(theReply.replacing) { err = FSMakeFSRefUnicode(&fsRefParent, nameLength, nameBuffer, kTextEncodingUnicodeDefault, &fsRefDelete); if(err == noErr) err = FSDeleteObject(&fsRefDelete); if(err == fBsyErr) { DisposePtr((Ptr)nameBuffer); return err; } } err = FSCreateFileUnicode(&fsRefParent, nameLength, nameBuffer, kFSCatInfoNone, NULL, NULL, theFileSpec); err = FSpGetFInfo(theFileSpec, &fileInfo); fileInfo.fdType = fileType; fileInfo.fdCreator = fileCreator; err = FSpSetFInfo(theFileSpec, &fileInfo); return err; }
So now my original scrap of code would become:
long byteCounter; StandardFileReply reply; FSSpec asciFile; short asciFileNum; char *textData; OSErr err = noErr; err = SimpleNavPutFile('TEXT', 'XCEL' &asciFile); if(err == noErr) // a file was created { err = FSpOpenDF(&asciFile, fsCurPerm, &asciFileNum); if (err != noErr) return err; err = FSWrite(asciFileNum, &byteCounter, textData); if (err != noErr) return err; err = FSClose(asciFileNum); } return err;
I've sacrificed a little functionality in the dialog, since I can no longer suggest a default file name and prompt the forgetful user about what he is supposed to be doing. This scrap is fewer lines of code than my original, though, and even easier to use. I shall not attempt to explain what all the functions are doing in my SimpleNavPutFile. All I can say is that this works on my machine and is not meant to be anything other than a quick and dirty solution. Note that it includes the call creating the file and deals with the choice to replace an existing file. It may just amount to the same thing as using the now-deprecated NavPutFile, but I believe it lets OS-X put up the latest and greatest file navigation dialog.
I should confess that this solution will fail with OS-9 because it gives up if it cannot coerce the AEDesc to an FSRef. Bricknell's book has a discussion on page 960 of how to derive the FSSpec in OS-9 when this coercion fails. I have not included it, because my immediate concern was getting over the hump in OS-X, and I want to keep this as simple as possible.
The corresponding SimpleNavGetFile is built around
OSStatus NavCreateGetFileDialog ( const NavDialogCreationOptions * inOptions, NavTypeListHandle inTypeList, // can be NULL NavEventUPP inEventProc, // can be NULL NavPreviewUPP inPreviewProc, // can be NULL NavObjectFilterUPP inFilterProc, // can be NULL void * inClientData, // can be NULL NavDialogRef * outDialog );
Most of the parameters which can be set to NULL have system defaults that will be used when they are NULL. Setting the NavTypeListHandle to NULL simply results in no file filtering in the dialog. In order to avoid dealing with the NavDialogCreationOptions you can use NavGetDefaultDialogCreationOptions to set everything to a default.
SimpleNavGetFile(FSSpec *theFileSpec) { OSStatus theStatus; NavDialogRef theDialog; NavReplyRecord theReply; NavDialogCreationOptions inOptions; AEKeyword theKeyword; DescType actualType; Size actualSize; OSErr err = noErr; NavGetDefaultDialogCreationOptions(&inOptions); theStatus = NavCreateGetFileDialog(&inOptions, NULL, NULL, NULL, NULL, NULL, &theDialog); NavDialogRun(theDialog); theStatus = NavDialogGetReply ( theDialog, &theReply); NavDialogDispose(theDialog); if(!theReply.validRecord) { return -1; // Assuming the user changed his/her mind? // No harm; no foul, but need to know // not to try to open the file. } // Get a pointer to selected file err = AEGetNthPtr(&(theReply.selection), 1, typeFSS, &theKeyword, &actualType, theFileSpec, sizeof(FSSpec), &actualSize); return err; }
If you want to filter files to limit the options provided the user in the Navigation Dialog, you can either use an 'open' resource created with a resource editor or you can create a NavTypeList. To use an 'open' resource you get the NavTypeListHandle with a GetResoure function.
NavTypeListHandle typeList = (NavTypeListHandle)GetResource('open', 128); theStatus = NavCreateGetFileDialog(&inOptions, typeList, NULL, NULL, NULL, NULL, &theDialog);
The 128 is just the number of the resource you have created. If no such resource is found typeList will be given a NULL value and you will be doing no file filtering.
To create your own NavTypeList from scratch you need only fill in a few blanks.
NavTypeList inTypeList; NavTypeListPtr inTypeListPtr; inTypeList.componentSignature = kNavGenericSignature; inTypeList.osTypeCount = 1; inTypeList.osType[0] = 'TEXT'; inTypeListPtr = &inTypeList; theStatus = NavCreateGetFileDialog(&inOptions, &inTypeListPtr, NULL, NULL, NULL, NULL, &theDialog);
I have used a NavTypeListPtr just to minimize the confusion when it is necessary to pass a NavTypeListHandle to NavCreateGetFileDialog. The kNavGenericSignature is a system constant which tells it not to filter files by their creator. If you wanted to choose from only Excel files you could put 'XCEL' here instead. Since the osType is an array you can list several file types for a given appliction signature, and you can also make NavTypeList itself an array so that the handle tells the dialog to display any number of file types from any number of specifica applications. If you need to do this, though, you are on your own.
Richard Patterson is in charge of digital imaging at Illusion Arts, a visual effects facility in Van Nuys, CA, specializing in matte paintings and bluescreen compositing for movies. You can reach him at richard@illusion-arts.com.
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine