MultiFile dialog
Volume Number: 7
Issue Number: 5
Column Tag: C Forum
File Mgr (PBxxx)
MultiFile Dialog 
By Eric Schlegel, Castleton, NY
Note: Source code files accompanying article are located on MacTech CD-ROM orsource code disks.
[Eric Schlegel attends Dartmouth College in Hanover, NH, and he has been
programming for the past five years with most of his work in MPW C and Pascal.]
The Standard File Package is one of the most useful parts of the Macintosh
Toolbox. It allows a programmer to easily get a filename from a user, and it presents a
simple, intuitive interface with which a user can select a file. A key aspect of the
package, however, is that it is designed to select only a single file. It is not designed to
select multiple files, which can be a problem for some applications - in particular,
mine. In this article, I’ll present my solution to the problem, in the form of a MultiFile
Select dialog.
MPW’s Commando dialogs were the original model for the MultiFile Select dialog.
MPW is a command-line environment, so that to open a file, you type something like
“open file1.c file2.c file3.c.” Typing many long filenames is tiring and error-prone. To
remedy this problem, Apple introduced in MPW 2.0 the Commando interface, which is
used as a front end to the command line. In particular, the Commando interface has built
into it a multifile select dialog just like mine. Now you can select multiple files using
Commando and Commando will automatically generate the commands to, say, open the
files.
This interface was just what I needed for my own work, and so I set out to
replicate Commando’s multifile interface for my own program. The result is the
subject of this article.
What the User Sees
The MultiFile Select dialog appears to the user to be very similar to the standard
SFGetFile dialog (see Figure 1). The user can still scroll through a list of files, move
into and out of folders, change drives, etc. There are two major differences from the
standard dialog:
1. The Open button has been renamed Done.
2. Beneath the standard list of files is another file list; to the right of this list are
two buttons, Add/Open and Remove.
Figure 1. The Dialog Box
The user adds a file to the lower list box by selecting a file in the upper box and
clicking on the Add button. Double-clicking or pressing Return or Enter will also add
the file to the list. If a folder is selected in the upper box, the Add button is renamed
Open, and clicking on it will simply open the folder. Files can be removed from the
lower box by selecting them and clicking on the Remove button. Once a list of files has
been collected, the user clicks on the Done button and the dialog closes.
Technical Background
The key to adding your own controls to a Standard File dialog is the dlgHook
parameter of the Standard File calls. This parameter, if not null, should be the address
of a function with the following declaration:
{1}
FUNCTION MyDlg (item: INTEGER; theDialog: DialogPtr) : INTEGER;
Standard File uses ModalDialog to run the Standard File dialog. After ModalDialog
returns, Standard File calls your dlgHook function, passing it the item number
returned by ModalDialog and the DialogPtr for the Standard File dialog. After handling
the event, the dlgHook function returns an item number to Standard File. You can add
your own controls to the Standard File dialog and handle hits on them with your dlgHook
function.
In addition to passing you item numbers from ModalDialog, Standard File also
passes your dlgHook several fake numbers. These fake numbers include:
-1 Passed to you only once when the dialog is being initialized. When you get
this event, you should do any initialization you need, such as changing the
name of a button in the standard dialog.
100 A null event. Nothing has happened.
101 Return this event from your dlgHook function to make Standard File redraw
the file list. This is useful if, say, you have radio buttons controlling what
type of files are displayed. When the file type changes, return 101 to
display a new list of files.
102 Generated by a click in the current directory button. It causes the directory
menu to be pulled down and tracked.
103 Generated by a double-click on a folder or a click on the Open button while
a folder is selected. It causes the folder to be opened.
1000+ ? Generated by a keyDown event. “?” is the ASCII code for the key that was
struck. 1065, for example, would indicate that the user had hit the ‘A’ key,
which has a code of 65.
For some applications even the dlgHook may not be enough to support your
modifications to standard file. This is when SFPGetFile and SFPPutFile come to the
rescue. These calls behave exactly like SFGetFile and SFPutFile except that they allow
you to use your own dialog resource and, more importantly, install a filter proc that
will be passed to ModalDialog. This filter proc has the same declaration as any other
ModalDialog filter proc:
{2}
FUNCTION MyFilter (theDialog: DialogPtr; VAR theEvent: EventRecord;
VAR itemHit: INTEGER) : BOOLEAN;
Using your filter proc you can examine and modify events before Standard File
even has a chance to look at them.
Programmer’s Interface
The file mfile.h is a header file for MultiFile; mfile.c contains the actual code.
Mfile.c first has #defines and global variables. The only interesting items are
FSFCBLen, SFSaveDisk, and CurDirStore, which are #defines to allow access to the
low-memory globals of the same names. FSFCBLen is -1 if MFS is running, or greater
than 0 if HFS is running. SFSaveDisk contains the negative of the vrefnum of the
volume currently displayed by Standard File. CurDirStore is only used under HFS; it
contains the dirID of the directory currently displayed by Standard File.
The interface to the MultiFile dialog uses five procedures: get_tl, multifile,
num files, getfile, and freemf. Calling multifile is similar to calling SFGetFile except
that you cannot use your own dlgHook function. Multifile’s first argument is a point
indicating where to place the top left corner of the dialog; this argument is passed
directly through to SFPGetFile. Get_tl will calculate this point for you so that the dialog
is nicely centered on the screen. Multifile’s second argument is a C string which is used
as a prompt for the lower file list box. The third, fourth, and fifth arguments are a
filefilter proc, the number of types in your type list, and the type list itself. These
three parameters are passed directly to SFPGetFile, so that you can customize which
files are displayed in the upper file list.
Multifile itself is trivial. It first makes a copy of your prompt string so that the
string can be accessed by other procedures. It then reads in the strings “Add” and
“Open” from the resource fork, initializes some variables, and calls SFPGetFile. The
rest of the work is done by Standard File, and by the dlg_hook and filter functions which
I pass to SFPGetFile.
Numfiles returns the number of files collected. This number is stored in the
global variable nfiles after the dialog closes.
Getfile takes two arguments, a file number and a pointer to an FSTRUCT struct,
and returns in the struct the indicated file. The file number is one-based and may range
from one through the number returned by num files. An FSTRUCT is defined as follows:
/* 3 */
typedef struct fstruct {
OSType ftype;
short vrefnum;
short fver;
String(63) fname;
} FSTRUCT;
An FSTRUCT is the same as an ordinary SFReply record except that it does not
have a field to indicate whether the data is valid. Instead, multifile returns a Boolean
result of true if the user clicked on the Done button and false if the user clicked on the
Cancel button. Ftype is the file type. Vrefnum is a true volume refnum if you are using
MFS, or a working directory refnum if you are using HFS. Fver is the file version.
Fname is a Pascal string containing the file name.
Freemf deallocates memory used by multifile to hold the list of selected files. You
should call freemf once you’ve retrieved all the files with getfile.
Utility Routines
The dlg_hook and filter functions use a collection of utility routines. Most of
these are simple; the only interesting ones are makewd, mkcell, inlist, and sel1cell.
Makewd creates a new working directory for the current volume and directory using
PBOpenWD, and returns the wdrefnum. Before using PBOpenWD, however, makewd
must check that:
1. Multifile isn’t running under MFS. If so, makewd simply returns the current
volume refnum.
2. The current volume isn’t an MFS volume. If so, makewd again returns the
current vrefnum.
Mkcell takes information about a file and fills in an MFCELL struct for the file.
Mkcell also has to watch that we aren’t running under MFS; if we are, mkcell always
fills in 0 for the current directory ID. The HFS dirID comes from the global
CurDirStore, which isn’t used under MFS and probably contains trash. Mkcell always
uses 0 to maintain a constant value for the dirID under MFS.
Multifile passes its own SFReply record to SFPGetFile. Inlist returns true if the
file currently described by that record is in my list, and if it is, the cell that the file is
in. I use inlist to avoid adding a file to my list more than once. The tricky part about
inlist is its use of LSearch to look for the file. LSearch matches the data it is looking for
against the entire cell; in other words, if you are looking for “cde”, you will only find
cells with “cde” in them. You will not find cells with “abcde”, “cdefg”, or “abcdefg”.
To me, this means that I can’t just look for the name of the current file. Instead, I have
to set up an entire MFCELL struct with all fields filled in and look for a matching cell.
Sel1cell takes a cell in my file list and ensures that that cell alone is selected.
The best way I’ve found to do this is simply by looking for selected cells in my list and
deselecting them as I find them, and then selecting the specified cell. I’ve tried setting
the lOnlyOne flag in the list record and then selecting the single cell, but it doesn’t
work. I’m still trying to think of a better way to do this.
DlgHook Function
I use my dlgHook function to handle hits on my three added buttons, to receive
fake item numbers from Standard File, and to intercept hits on some of the standard
items in the Standard File dialog. Initdlg is called when the dlgHook function receives
-1 as the item number. Initdlg installs your prompt string in my extra staticText item,
installs a userproc to draw my file list and the border around the Add/Open button, and
calls the List Manager to create a new list. The only tricky part about initdlg is leaving
room for the vertical scroll bar when creating the new list.
When I get a hit on the Done or Cancel buttons I call disposdlg to clean up my part
of the dialog. Disposdlg first saves the number of files selected in the variable n files. At
this point, all data about the files is stored in my file list. I have to copy this
information to a safe place before I dispose of the list. Disposdlg allocates memory in
the form of an array of FSTRUCTs, one struct for each file. It then copies the
information from my file list into the newly allocated memory, and finally disposes of
the list. Once disposdlg returns, my dlgHook functions sets the variable good to true or
false, depending on whether Done or Cancel was hit. Multifile returns the value of good
when it finishes.
A hit on the Remove button is easy to handle. I use the List Manager to delete any
selected files in the lower list box.
A hit on the Add/Open button is a bit more complicated. I first check to see if the
current selection is a file or a folder. If it’s a folder, I return 103 from the dlgHook
function to make Standard File open the folder. Otherwise, I call do_add to add the file to
my file list box. Do_add first uses inlist to check that the file is not already in the list;
if it is, do_add returns without doing anything. If the file is not in the list, do_add calls
makewd to open a working directory for the file. Makewd returns the wdrefnum if it
succeeds, or 0 if too many working directories are already open. (The current limit on
open wd’s is 40.) If makewd returns 0, do_add uses wdalert to display an alert
informing the user. Otherwise, do_add calls addfile to add the file to my list. Do_add
then disables the Add button, since the selected file can’t be added to the list again, and
enables the remove button, since the new file is now selected in my file list.
My dlgHook function also has to watch out for hits on items in the original
Standard File dialog. The procedure chknmlist ensures that the title of the Add/Open
button always corresponds to the selection in the upper file list. If the selection is a
file, the button should read Add; if the file is already in my file list, the Add button
should be inactive and the file should be selected in my list as well as the original list
box. If the selection is a folder, the button should read Open. Virtually any hit on the
original Standard File dialog can change the selection, and so whenever a hit occurs I set
a flag variable, chklist, to indicate that chknmlist should be called to examine the
selection.
Setting a flag is necessary because a Standard File hit doesn’t always mean that
the selection has already changed. An example of this problem is the Eject button. The
current disk won’t be ejected until after I’ve seen the hit on the button; therefore, the
selection hasn’t yet changed at the time I get the hit. Instead of checking the selection
immediately I just set chklist to indicate that it needs to be checked. The next time my
ModalDialog filterProc is called, it looks at chklist and calls chknmlist if chklist is true.
The filterProc was not originally used for this purpose. In the original version
of MultiFile I looked at chklist whenever my dlgHook got a null event from Standard
File. This approach also works fine except for one small problem: if any window behind
the MultiFile dialog needs updating, Standard File will constantly receive update events
for that window and never get any null events. Your dlgHook, in turn, will never get
any null events either. In my case this meant that the selection would be changed but the
Add/Open button would never be updated. It was some consolation to me that Apple was
also bitten by this bug; see Tech Note 99.
FilterProc
My filterProc, filter, has few but important uses. Each time it is called it calls
chknmlist if chklist is true. On mouseDown events, it calls do_mousedown;
do_mousedown in turn calls LClick to track the mouse in my file list. On keyDown and
autoKey events it calls do_keydown. Do_keydown looks for Return or Enter and flashes
the Add button if it sees either one.
The List Box
The useritem in the MultiFile dialog is a placeholder for my file list box. The list
box is implemented using the List Manager and a custom list definition function. The list
is 2 columns wide and has one row for each file. Each cell in the first column is of type
MFCELL; this type is defined in the file mfpriv.h.
/* 4 */
typedef struct mfcell {
String(63) fname;
OSType ftype;
short fver;
short vrefnum;
short dirid;
} MFCELL;
Fname, ftype, and fver are the filename, file type, and file version, respectively.
Vrefnum is the volume refnum of the file’s volume under both MFS and HFS; it is never
a working directory refnum. Dirid is the directory id of the file’s directory under HFS;
under MFS it is unused and is set to 0. The second column of the list contains a working
directory refnum for the file’s directory, or a copy of vrefnum under MFS.
I use a custom list definition with code in the files mfldef.a and mfldef.c. Mfldef.a
is an Assembly header for the actual LDEF in mfldef.c. The file mfldef.make is an MPW
Make file to build the LDEF. The custom list definition is trivial. It first checks which
column the cell it is drawing is in. If the cell is in the first column, it draws the
filename; if the cell is in the second column, it returns without drawing, since the cell
only contains a wdrefnum.
Mousedowns in the list are caught by my filterProc function, which calls LClick
when it sees a mouseDown.
MultiFile’s resources are in the file mfile.r in MPW Rez format. I use my own
dialog resource and pass the resource id to SFPGetFile. My dialog is based on the
standard SFGetFile dialog. The ‘DLOG’ resource is somewhat taller to make room for my