TweetFollow Us on Twitter

File Finder DA for HFS
Volume Number:2
Issue Number:3
Column Tag:C Workshop

Lost File Finder DA for HFS

By Mike Schuster, Software Engineer, MacTutor Contributing Editor

Apple designed the new Macintosh Plus Standard File dialog package to provide an easy method for finding files nested within the hierarchical file structure of a mounted volume. At any given time, the dialog displays a sorted list of the files and folders contained within a "current folder". If the desired file is not in that folder, and therefore not in the displayed list, you must first find the folder that contains the desired file.

If you believe that the desired file is in one of the "sibling folders" listed, you select that sibling and click on the Open button. The selected folder becomes the new current folder, and its contents are displayed. You may have to repeat this process if the file is nested several levels deep.

On the other hand, if the file is not contained within any sibling of the current folder, then you choose some "ancestor" as a new current folder from the pop-up menu at the top of the dialog. You then either select the desired file from the list, or repeatedly open sibling and/or ancestor folders until you find the file.

Fig. 1 Moving up to an ancestor

This manual traversal of the hierarchy can be quite tedious if you have forgotten your file's location within the hierarchy. It reminds me of an Easter Egg hunt. When working with files in several folders, I find it quite easy to accidently save a file without noticing which folder the file was placed, especially after quickly typing its name and and an Enter key. Obviously, some sort of global search function would be handy. This is the subject of this month's article.

A User Interface for Global Search

Here's what I have in mind. As soon as an application displays an Open file dialog, I'd like to type the first few letters of my file's name. If the file is contained in the current folder, an Enter key is sufficently to complete the dialog. Otherwise, I'd like to enter some command key combination to force standard file to search the hierarchy for a file whose name matches the letters I've already typed.

As soon as standard file finds a candidate, it should automatically make the folder containing the candidate the current folder as well as select the candidate. At this point, I can either accept the candidate by typing an Enter key, or resume the search by entering the command key combination once again. I'd like to be able to repeat this process until either standard file has exhaustively searched the whole file hierarchy, or until I find the desired file.

In retrospect, such a scheme might take a while on a large file system, so some way of aborting the search should be available. That way, if I get tired waiting, I can always go back and hunt for eggs myself.

A Design using a Dialog Hook

When I first considered an implementation for such a global search, it seemed that rather large changes and additions to the standard file package might be required. This turned out not to be the case. I was able to implement all of the above user interface without any modifications to the standard file package using a C language routine passed as the filterProc argument to the SFPGetFile trap!

My filterProc routine implements the search by generating a series of "synthetic" events that cause standard file to sequentially traverse the file hierarchy until a candidate match is found. You can think of it as a simple state machine that methodically selects each file and folder from the list one at a time in turn. Periodically, the machine uses the Open button and pop-up menu at appropriate times to select new folders in which to continue the search. As soon as a candidate file is found, filterProc stops creating synthetic events and begins sending null events through unchanged, allowing the user to accept the selection, continue the search, or use any of the dialog's buttons and controls in the standard way.

Rather than generating a sequence of mouse down events in the appropriate dialog controls, the filterProc generates the following set of cursor key events, which are understood by the new standard file package:

Fig. 2 Command key selections

Typing a Tab key is equivalent to clicking on the Drive button. Typing an down or up arrow key is equivalent to scrolling the list of files and folders and selecting the next item below, or previous item above in the list, respectively. If the currently selected item is a folder, then the Command Ø combination is equivalent to clicking on the Open button. Similarly, the Command combination is equivalent to selecting the current folder's immediate parent from the pull-down menu.

In addition to implementing cursor keys, the new standard file package dynamically updates the fields of its standard file reply record argument while the dialog is open:

Fig. 3 Standard File Reply Record Argument

If the currently selected item in the dialog is a file, then the reply's file type and file name length fields are both postive. In this case, the file type field contains the four letter finder document type of the file. If the selected item is a folder, only the file type field is positive. It contains the folder's file catalog directory identifier. Finally, if neither a file nor a folder is selected, both the file type and name length fields are zero.

This information, combined with the directory identifier of the current folder (which is contained in a low memory global location), provide filterProc with just enough information to allow it to traverse the hierarchy via a sequence of cursor keys. It accomplishes the global search without a single I/O call to the file system! One nice side effect of this scheme is the animation of the dialog as folders are opened and closed and as the item's themselves are selected and scrolled.

The "Lost File Finder" Desk Accessory

The Lost File Finder desk accessory contains an implementation of my global search scheme. The accessory presents a standard file dialog listing all of the files in the current folder. After typing a few letters, you enter the Command-space combination to begin (or continue) a search. The Command-period combination aborts a search. Since standard file displays the last current folder when opening a new dialog, you can use the accessory first to find the folder containing your file, and then use your application's open command to open it.

The filterProc routine in the accessory is composed of three parts. The first parts saves key events in a key buffer. This buffer is used to find potential candidate files. The second part handles the Command-space and Command-period commands. The third part is a simple state machine that generates the approprate cursor key events needed to traverse the file catalog.

The state machine is built from three states, named NULLSTATE, NEXTSTATE, and SEARCHSTATE. In NULLSTATE, null events are sent through unchanged. In NEXTSTATE, successive items in the current folder are selected. If the selected item is a file, its name is compared with the contents of the key buffer. If the selected item is a folder, then it is made the new current folder. When the end of the item list is encountered, the current folder is closed and its parent is opened. At this time, the state machine moves to SEARCHSTATE, in which successive items in the parent's folder are skipped until the just closed folder is encountered. Then the state machine returns to NEXTSTATE, and once again begins considering successive items. Of course, the root folder is handled as a special case.

In NEXTSTATE, when a file item is selected, filterProc compares its name with the contents of the key buffer. If they match, the state machine moves to NULLSTATE, and filterProc waits for further input from the user. Otherwise, the state machine stays in NEXTSTATE and continues the search.

The filterProc routine maintains two extra reply records named lastreply and startreply. Lastreply contains information on the previous item selected. It is used to detect the end of the item list, and is required since a Ø at when the last item in the list is selected has no effect. Startreply contains information on the item that was selected when the global search was begun. It is used to terminate the search when the file hierarchy has been completely traversed.

The accessory accesses three global low memory values. ISHFS is nonzero if the Hierarchical File System is installed on the Macintosh.

SFFolder contains the directory identifier of the current folder. KEYTHRESH contains the current keyboard repeat threshold value, which is used to flush the contents of the key buffer at approprate intervals.

In addition to these values, the accessory modifies the location

*(int *) (thedialog->refcon - 624)

where thedialog is a pointer to standard file's dialog window. The event record's modifiers field is returned by filterProc in this location. The dialog's refcon contains a copy of standard file's frame pointer register A6. I discovered the proper offset value by disassembling the routine that standard file passes to ModalDialog as its own filterProc.

The accessory will only work with the standard file and system folder distributed with the Macintosh Plus. The system folder distributed with the original HD-20 apparently does not contain the recent extensions to standard file. You can use the Mac Plus system folder on an original 512K Mac, but not on a 128K Mac. I'm using the system folder contained on the "Macintosh Plus Programmer's Package, Mac Disk 1, Jan 1986" distributed by Apple at the January Developers Conference. It works find in the startup drawer of my General Computer HyperDrive internal hard disk.

The following is the C language implementation of the Lost File Finder accessory. Since it contains no assembly language routines, it should be easy to port to your personal development system. One thing to watch out for, however, is that the accessory's open routine closes the accessory with a call on CloseDeskAcc. Make sure that your development system's desk accessory interface glue can handle this gracefully.


char keys[MAXKEYS + 1]; /* key buffer (pascal string) */

extern boolean getreplyeventfilter();

/* desk accessory open routine */
int accopen(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 point where;
 
 /* initialize state machine */
 state = NULLSTATE;
 *keys = 0;
 keywhen = 0l;
 
 /* display dialog */
 where.a.h = 82; 
 where.a.v = 50;
 sfpgetfile(&where, "", 0l, -1, 0l, 0l, &reply, -4000, &getreplyeventfilter);
 
 /* close ourselves */
 closedeskacc(dctl->dctlrefnum);
 
 return 0;
/*
 * lost file finder, version 1.0
 * find lost files with standard file
 *
 * copyright (c) 1986 by mike schuster for MacTutor.
 * all rights reserved.
 */

/* macintosh headers */
#include <acc.h> /* This file published last month  */
#include <dialog.h>
#include <device.h>
#include <event.h>
#include <qdvars.h>

/* c headers */
#include <string.h>
#include <ctype.h>

/* desk accessory header */
ACC
 (
 0x0400,/* accctl */
 0,/* no seconds */
 0,/* no events */
 0,/* no menu */
 16,    /* length */
 "Lost File Finder " /* title */
 )

/* standard file key event offset */
#define KEYOFFSET 4096

/* states for filtering machinery */
#define NULLSTATE 0
#define NEXTSTATE 1
#define SEARCHSTATE 2

/* standard cursor control keys */
#define NEXTKEY 0x1f
#define PREVKEY 0x1e
#define DOWNKEY -0x1f
#define UPKEY -0x1e
#define HOMEKEY 0x1d
#define SEARCHKEY ' '
#define QUITKEY '.'


#define isfile(reply) (reply)->fname[0]
 /* nonzero if file selected */
#define isfolder(reply) !(reply)->fname[0]   
 /* nonzero if folder selected */
#define isnull(reply) (!(reply)->ftype && !(reply)->fname[0]) 
 /* nonzero if nothing selected */
#define ISHFS (*(int *) 0x3f6 > 0) 
 /* nonzero if hfs installed */
#define SFFOLDER *(long *) 0x398   
 /* current standard file folder */
#define KEYTHRESH *(int *) 0x18e   
 /* keyboard repeat threshold */
#define ROOTFOLDER 2 
 /* directory id of root folder */

/* standard file reply typedef, with ftype defined as long */
typedef struct
 {
 boolean good;
 boolean copy;
 long ftype;
 int vrefnum;
 int version;
 char fname[64];
 } sfreply;

sfreply reply;   /* current standard file reply */
sfreply lastreply; /* last standard file reply */
sfreply startreply;/* starting standard file reply */
long startfolder;/* starting folder */
long searchfolder; /* folder to search for */
int state;/* current state of filtering machinery */

#define MAXKEYS 24 /* maximum length of key buffer */
long keywhen;    /* time of last keydown */
char keys[MAXKEYS + 1]; /* key buffer (pascal string) */


extern boolean getreplyeventfilter();

/* desk accessory open routine */
int accopen(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 point where;
 
 /* initialize state machine */
 state = NULLSTATE;
 *keys = 0;
 keywhen = 0l;
 
 /* display dialog */
 where.a.h = 82; 
 where.a.v = 50;
 sfpgetfile(&where, "", 0l, -1, 0l, 0l, &reply, -4000, &getreplyeventfilter);
 
 /* close ourselves */
 closedeskacc(dctl->dctlrefnum);
 
 return 0;
 }

/* null desk accessory close routine */
int accclose(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 return 0;
 }

/* null desk accessory control routine */
int accctl(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 return 0;
 }

/* null desk accessory prime routine */
int accprime(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 return 0;
 }

/* null desk accessory status routine */
int accstatus(dctl, pb)
 dctlentry *dctl;
 ptr pb;
 {
 return 0;
 }


/* case insensitive pascal string compare, return zero */
/* if equal. If prefix is nonzero, then a and b are equal  */
/* if a is a prefix of b */
int pstrcmp(a, b, prefix)
 register char *a;
 register char *b;
 int prefix;
 {
 register int n;
 
 if (prefix ? *a > *b : *a != *b)
 return 1;
 for (n = *a++ & 0xff, b++; n && tolower(*a++) == tolower(*b++); n--)
 ;
 return n;
 }

/* copy a standard file reply */
sfreply *replycpy(a, b)
 sfreply *a;
 sfreply *b;
 {
 blockmove(b, a, (long) sizeof(sfreply));
 return a;
 }

/* compare two standard file replies, return zero if equal */
int replycmp(a, b)
 sfreply *a;
 sfreply *b;
 {
 return (a->ftype == b->ftype) ? pstrcmp(a->fname, b->fname, 0) : 1;
 }

/* indicate a failure by sounding off */
int fail(thestate)
 int thestate;
 {
 sysbeep(4);
 return thestate;
 }

/* return a key to standard file, and update the current state */
int theitemhit(thedialog, itemhit, thekey, thestate)
 windowrecord *thedialog;
 int *itemhit;
 int thekey;
 int thestate;
 {
/* save synthetic event modifiers word in proper place */
*(int *) (thedialog->refcon - 624) = thekey < 0 ? cmdkey : 0;

/* return the desired key with the appropriate itemhit offset */
*itemhit = KEYOFFSET + (thekey < 0 ? -thekey : thekey);

 /* update the current state and return */
 state = thestate;
 return -1;
 }

/* get standard file reply event filter */
pascal boolean getreplyeventfilter(thedialog, theevent, itemhit)
 windowrecord *thedialog;
 eventrecord *theevent;
 int *itemhit;
 {
 int c;
 
 switch (theevent->what)
 {
 case nullevent:
 switch (state)
 {
 /* look for the last opened folder in parent's item list */
 /* change to NEXTSTATE when found */
 case SEARCHSTATE:
 if (isfolder(&reply) && reply.ftype == searchfolder)
 {
 replycpy(&lastreply, &reply);
 state = NEXTSTATE;
 }
 return theitemhit(thedialog, itemhit, NEXTKEY, state);
 break;
 
 /* search for candidate files */
 case NEXTSTATE:
 /* handle end of file/folder list */
 if (isnull(&reply) || !replycmp(&reply, &lastreply))
 {
 /* return to first item in list in a flat file catalog */
 if (!ISHFS)
 return theitemhit(thedialog, itemhit, HOMEKEY, fail(NULLSTATE));

 /* return to first item in list if in root of a hierarchical file catalog 
*/
 else if (SFFOLDER == ROOTFOLDER)
 {
 if (SFFOLDER == startfolder && !replycmp(&reply, &startreply))
 state = fail(NULLSTATE);
 replycpy(&lastreply, &reply);
 return theitemhit(thedialog, itemhit, HOMEKEY, state);
 }

 /* otherwise return to parent folder and search for current folder */
 else
 {
 searchfolder = SFFOLDER;
 return theitemhit(thedialog, itemhit, UPKEY, SEARCHSTATE);
 }
 }
 
 /* handle a selected file */
 else if (isfile(&reply))
 {
 /* check to see if complete catalog was searched */
 if (SFFOLDER == startfolder && !replycmp(&reply, &startreply))
 state = fail(NULLSTATE);

 /* check to see if a candiate was found */
 else if (!pstrcmp(keys, reply.fname, 1))
 state = NULLSTATE;

 /* otherwise, continue the search */
 else
 {
 replycpy(&lastreply, &reply);
 if (isnull(&startreply))
 replycpy(&startreply, &reply);
 return theitemhit(thedialog, itemhit, NEXTKEY, state);
 }
 }
 
 /* handle a selected folder */
 else
 {
 /* check to see if complete catalog was searched */
 if (reply.ftype == startfolder && !replycmp(&reply, &startreply))
 state = fail(NULLSTATE);

 /* otherwise, open the sibling folder and continue the search */
 else
 {
 if (isnull(&startreply))
 replycpy(&startreply, &reply);
 return theitemhit(thedialog, itemhit, DOWNKEY, state);
 }
 }
 break;
 default:
 break;
 }
 break;
 
     case keydown:
 /* handle keydown event */
 c = theevent->message & 0xff;
 
 /* if not command key, place in key buffer */
 if (!(theevent->modifiers & cmdkey))
 {
 /* reset buffer if threshold has expired */
 if (theevent->when > keywhen + KEYTHRESH)
 *keys = 0;
 
 /* add to key buffer */
 if ((*keys & 0xff) < MAXKEYS)
 {
 keys[(*keys & 0xff) + 1] = c;
 (*keys)++;
 }
 
 /* update time */
 keywhen = theevent->when;
 }
 
 /* handle the initiation of a search */
 else if (c == SEARCHKEY)
 {
 /* initialize last reply, starting folder and reply */
 replycpy(&lastreply, &reply);
 startfolder = SFFOLDER;
 replycpy(&startreply, &reply);
 
 /* force nextkey and initialize state machine */
 return theitemhit(thedialog, itemhit, NEXTKEY, NEXTSTATE);
 }
 
 /* handle the termination of a search */
 else if (c == QUITKEY)
 return theitemhit(thedialog, itemhit, NEXTKEY, fail(NULLSTATE));
 break;
 }
 return 0;
 }

A New Standard File

Since my global search requires the addition of a dialog hook, it can't be easily retrofitted into existing applications like MacWrite and MacPaint. The obvious solution is to append the filterProc code to the end of the standard file package resource and insert in the interface the appropriate interface calls to it. Of course, if a search capability were build directly into standard file, it might be nice, for speed, to avoid the dialog animation as the search proceeds.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Is there cross-platform play in slither....
So you've sunken plenty of hours into crawling around in slither.io on your iPhone or iPad. You've got your stories of tragedy and triumph, the times you coiled four snakes at one time balanced out by the others when you had a length of more than... | Read more »
Rodeo Stampede guide to running a better...
In Rodeo Stampede, honing your skills so you can jump from animal to animal and outrun the herd as long as possible is only half the fun. Once you've tamed a few animals, you can bring them home with you. [Read more] | Read more »
VoxSyn (Music)
VoxSyn 1.0 Device: iOS Universal Category: Music Price: $6.99, Version: 1.0 (iTunes) Description: VoxSyn turns your voice into the most flexible vocal sound generator ever. Instantly following even subtle modulations of pitch and... | Read more »
Catch Battleplans on Google Play from Ju...
Real-time strategy title Battleplans is due for release on Google Play on June 30th, following its release for iOS systems last month. With its simple interface and pretty graphics, the crowd-pleaser brings a formerly overlooked genre out for the... | Read more »
iDoyle: The interactive Adventures of Sh...
iDoyle: The interactive Adventures of Sherlock Holmes - A Scandal in Bohemia 1.0 Device: iOS Universal Category: Books Price: $1.99, Version: 1.0 (iTunes) Description: Special Release Price $1.99 (Normally $3.99) | Read more »
Five popular free apps to help you slim...
Thanks to retail and advertising, we're used to thinking one season ahead. Here we are just a week into the summer and we're conditioned to start thinking about the fall. [Read more] | Read more »
How to ride longer and tame more animals...
It's hard to accurately describe Rodeo Stampede to people who haven't seen it yet. It's like if someone took Crossy Roadand Disco Zoo and put them in a blender, yet with a unique game mechanic that's still simple and fun for anyone. [Read more] | Read more »
Teeny Titans - A Teen Titans Go! Figure...
Teeny Titans - A Teen Titans Go! Figure Battling Game 1.0.0 Device: iOS Universal Category: Games Price: $3.99, Version: 1.0.0 (iTunes) Description: Teeny Titans, GO! Join Robin for a figure battling RPG of epic proportions! TEENY... | Read more »
NinjAwesome: Tips and tricks to be a mor...
Sorry about that headline, but I'm going to go ahead and assume that GameResort would not have named its game NinjAwesome without expecting some of that. It is, in fact, pretty awesome the way it combines an endless runner and old school arcade... | Read more »
Into Mirror (Games)
Into Mirror 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: "Is all that we see or seem, but a dream within a dream?"- Edgar Allan Poe New game by Lemon Jam Studio, the team behind Pursuit... | Read more »

Price Scanner via MacPrices.net

13-inch Retina MacBook Pros on sale for up to...
B&H Photo has 13″ Retina MacBook Pros on sale for up to $150 off MSRP. Shipping is free, and B&H charges NY tax only: - 13″ 2.7GHz/128GB Retina MacBook Pro: $1179 $120 off MSRP - 13″ 2.7GHz/... Read more
Apple refurbished Mac minis available for up...
Apple has Certified Refurbished Mac minis available starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: - 1.4GHz Mac mini: $419 $80 off MSRP - 2.6GHz Mac... Read more
13-inch 2.5GHz MacBook Pro on sale for $999,...
B&H Photo has the 13″ 2.5GHz MacBook Pro on sale for $999 including free shipping plus NY sales tax only. Their price is $100 off MSRP. Read more
Apple refurbished iMacs available for up to $...
Apple has Certified Refurbished 2015 21″ & 27″ iMacs available for up to $350 off MSRP. Apple’s one-year warranty is standard, and shipping is free. The following models are available: - 21″ 3.... Read more
15-inch Retina MacBook Pros on sale for $200-...
B&H Photo has 15″ Retina MacBook Pros on sale for up to $210 off MSRP. Shipping is free, and B&H charges NY tax only: - 15″ 2.2GHz Retina MacBook Pro: $1799.99 $200 off MSRP - 15″ 2.5GHz... Read more
Mac minis on sale for up to $100 off MSRP
B&H Photo has Mac minis on sale for up to $100 off MSRP including free shipping plus NY sales tax only: - 1.4GHz Mac mini: $449 $50 off MSRP - 2.6GHz Mac mini: $649 $50 off MSRP - 2.8GHz Mac mini... Read more
Clearance 2015 13-inch MacBook Airs available...
B&H Photo has clearance 2015 13″ MacBook Airs available for $300 off original MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 1.6GHz/4GB/128GB MacBook Air (MJVE2LL/A): $799.... Read more
Apple refurbished Mac minis available for up...
Apple has Certified Refurbished Mac minis available starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: - 1.4GHz Mac mini: $419 $80 off MSRP - 2.6GHz Mac... Read more
ABBYY TextGrabber: 1,000,000 Installs in 5 Da...
ABBYY, an international OCR technologies provider, has announced that their image-to-text application TextGrabber, got installed 1,000,000 times in just five days while being featured by the App... Read more
New SkinIt Waterproof Case For iPhone 6
With its impact and waterproof design, the Skinit Waterproof case provides security and protection to guarantee your phone will get you through even the most demanding outdoor conditions. The impact-... Read more

Jobs Board

*Apple* iPhone 6s and New Products Tester Ne...
…we therefore look forward to put out products to quality test for durability. Apple leads the digital music revolution with its iPods and iTunes online store, Read more
*Apple* Retail - Multiple Positions, Towson...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* iPhone 6s and New Products Tester Ne...
…we therefore look forward to put out products to quality test for durability. Apple leads the digital music revolution with its iPods and iTunes online store, Read more
Music Marketing Lead, iTunes & *Apple*...
…Music Marketing Lead is responsible for developing robust marketing campaigns and programs for Apple Music and iTunes across the whole of Apple ecosystem. This Read more
*Apple* Valley Medical Clinic is Hiring - AP...
Apple Valley Medical Clinic is Hiring! Apple Valley Medical Clinic is an independently owned practice operating a Family Medicine Clinic, a 24/7 Urgent Care, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.