[comp.sys.mac.programmer] Sample code for placing a popup in SFPutFile

greggor@apple.com (Greg L. Anderson) (10/07/90)

It just so happened that I also had the need to put a popup menu item into
SFPutFile to select file formats for a program that I am working on.

The previously posted sample code helped a little bit (actually, it helped
a lot -- I didn't see the "item = -1" initialization event documented
in Inside Macintosh anywhere.  References, anyone?), but it was uncommented,
program-specific and hard to read.

Here's what I threw together today.  It is a _very_ robust piece of
code:  well commented, program-independant and in complete compliance
with the Macintosh Human Interface Guildlines.  (When you work at Apple,
you HAVE to put little triangles in your popup menus, or people say
horrible things about your code.  :> )

Documentation is included at the head of the file.  I strongly recommend
this code as a solid example of HIG-style popup usage.

Take a look & let me know what you think.  I hope you find it useful.

(Code follows .signature after linefeed)


  ___\    /___               Greg Anderson              ___\    /___ 
  \   \  /   /         Advanced Technology Group        \   \  /   /
   \  /\/\  /            Apple Computer,  Inc.           \  /\/\  /
    \/    \/               greggor@apple.com              \/    \/


/*===================================================================
==  SFPopupPutFile.c
==
==  Copyright 1990 Greg Anderson
==  greggor@apple.com
==
==
==  Permission granted to use, modify and distribute these sources
==  as much as possible.  Please inform the original author of any
==  significant enhancements.
==
==
==  These routines implement a version of SFPutFile that includes
==  a popup menu that is used to select a file format for saving.
==  The Macintosh Human Interface Guildlines for popup menus are
==  followed exactly; the popup's title is highlighted while the
==  popup is displayed, the current selection is checked, and the
==  popup is labled with a downward-facing triangle when it is not
==  popped up.
==
==  Additionally, the cursor is transformed into an I-Beam when
==  it passes over the editable text field.  Copy, cut and paste
==  are supported via Command-X, C and V, or the equivalent
==  function keys on an extended keyboard.  Return and Enter are
==  interpreted as "Save", and Command-. is interpreted as "Cancel".
==  If one of these key-equivalents is pressed, the appropriate
==  button is hilighted momentarily before the dialog box is closed.
==  (The filterProc that controls these functions will port without
==  modification to any dialog box that calls ModalDialog() and
==  has item #1 = Okay (or equivalent) and item #2 = Cancel.  Editable
==  text fields may appear anywhere & in any number.)
==  
==
==  In order to use SFPopupPutFile, you must create a custom dialog
==  box that is very similar to the standard SFPutFile dialog, but
==  has two additional items:  a static text item that says
==  "File Format:" and a user item that defines the location of the
==  popup menu.
==
==  The easiest way to do this is to copy the standard SFPutFile
==  dialog out of the system file, paste it into your own resource
==  fork and edit it there.  To be safe, copy your system file and
==   use ResEdit to extract a copy of the dialog from the copy of
==  the system.
==
==  SFPutFile's dialog box ID is -3999.
==
==  You can change the ID of your dialog box to something else, if
==  you wish.  I choose to use ID 3999 because it is similar to the
==  default ID without looking like an 'owned resource' to ResEdit.
==  (Little things like that make me nervous, you know?  :> )  If
==  you use an ID other than 3999, be sure to change the
==  #define putFileID (found right after this comment block) to
==  the appropriate value.  If you change the ID of the DITL to match
==  the ID of the DLOG (a good idea), don't forget to change the
==  DLOG's 'item list' ID to the new value.  (Do this with the
==  "Show As Text" menu item in the DLOG menu in ResEdit 1.2 and 2.0).
==
==  You may change the #defind putPopupItem to be something other
==  than 10 if you would like to put some other objects before the
==  useritem, but you *MUST* make the static text item one number
==  LOWER than the user item, as this is the item that the function
==  'FileFormatPopup' inverts when the popup menu is displayed.
==  Other than that, you have some flexibility in the layout of your
==  dialog box.  It is legal to move the items already defined
==  (#s 1-8), but I don't recommend it, as Macintosh users expect
==  SFPutFile to look a certain way.  I made the following changes to
==  my SFPopupPutFile dialog box:
==
==  	o	I increased the height of the box to 224 to make
==  		room for the popup item
==
==  	o	I created static text item #9 and made its bounding
==  		rectangle (16,192,100,208).  (Left,top,right,bottom)
==
==  	o	I created useritem item #10 and made its bounding
==  		rectangle (100,191,270,210).  (Left,top,right,bottom)
==
==  The idea is to make your useritem just a little bit wider than your
==  popup menu will be (to leave room for the downward-facing triangle,
==  which should hang over the edge of your popup).  The width of your
==  popup menu is determined by the length of the longest string it
==  contains.  (The longest string in my popup is 19 characters long,
==  and my useritem is 170 pixels across.)  You may need to experiment
==  a little to get the propper sized useritem.
==
==  At the end of this file is a BINHEX'ed version of the dialog box I
==  created by modifying the default dialog found in the system file.
==  You might save some time by starting with these resources and
==  editing them to suit your application.
==
==  Once you have your custom dialog box, calling SFPopupPutFile is
==  almost exactly like calling SFPutFile:
==
==
==  OSType SFPopupPutFile(	Point where, Str255 prompt,
==  				Str255 originalName,
==  				SFReply *reply, OSType defaultType,
==  				FFormList *formats )
==
==
==  The first four parameters are exactly the same as those given
==  in Inside Macintosh I for SFPutFile:
==
==  	where			The location to put the dialog box
==  	prompt			Message for the user
==  	originalName		The default name to save the file under
==  	reply			See SFPutFile
==
==  The other parameters are unique to SFPopupPutFile:
==
==  	defaultType		Specifies (by type) the default format to
==  				display in the popup menu
==  	formats			A list of all file formats that will appear
==  				in the popup menu when the user clicks on it
==
==  On exit, SFPopupPutFile returns the OSType of the file format
==  selected in the popup menu.  Your program should remember this
==  format only if the user clicked on 'Save' (reply.good == true).
==
==  The 'formats' variable is of type FFormList:
==
==  	typedef struct _fformList
==  	{
==  		char	*name;
==  		OSType	typeID;
==  	} FFormList;
==
==  "Name" is the ASCII text that identifies the file type to the
==  user when the popup menu comes up.  'typeID' is a value that
==  identifies the file format to SFPopupPutFile and your program.
==  If all of your file formats are saved with a different file
==  type, then the typeID of each format should be its file type.
==  (This will make your life easier.)  HOWEVER, each format type
==  *MUST* have a unique typeID, or SFPopupPutFile won't be able
==  to tell them apart (and neither will your program, as
==  SFPopupPutFile returns the typeID from your FFormList to
==  identify the file format!)  The list of file formats is
==  terminated by an entry with a null typeID (0L).
==
==  In my program, every file format is saved with the file type
==  'TEXT', and the typeID has no relation to the file type.  If your
==  program saves different file types AND sometimes saves different
==  formats with the same file type, then you will have to write
==  a routine to convert typeID's to file types & not use your file
==  types as your typeID's.
==
==  Here is the FFormList from my program, Pon Nuki:
==
==  	#define FORM_PONNUKI		'PNUK'
==  	#define FORM_ISHI		'ISHI'
==  	#define FORM_SMARTGO		'SMRT'
==  	#define FORM_COSMOS		'COSM'
==
==  	FFormList ponNukiFormats[] =
==  	{
==  		"Cosmos Format",		FORM_COSMOS,
==  		"Ishi Press Standard",		FORM_ISHI,
==  		"Pon Nuki Format",		FORM_PONNUKI,
==  		"Smart Go Format",		FORM_SMARTGO,
==  		NULL,				0
==  	};
==
==
==  SFPopupGetFile is left as an exercise for the reader.  :>
===================================================================*/
#include <MacTypes.h>
#include <ResourceMgr.h>
#include <StdFilePkg.h>
#include <FileMgr.h>
#include <DialogMgr.h>
#include <ControlMgr.h>
#include <pascal.h>

/*
 *  Some constants:
 */
#define putFileID			3999
#define putPopupItem			10
#define popupMenuID			23456

/*
 *  This global temporarily stores the file format that is displayed
 *  in the popup item we added to SFPPutFile.
 *
 *  It would have been nice to avoid a global, but I saw no way
 *  around it (as it is not possible to set the RefCon of SFPPutFile's
 *  dialog box -- doing so crashes the system).
 */
OSType		currentFileFormat;

typedef struct _fformList
{
	char	*name;
	OSType	typeID;
} FFormList;

/*
 *  Another global is used to keep track of where the FFormList is.
 */
FFormList	*fileFormats;

/*----------------------------------------------------------------------
|  Draw a shaded rectangle.  The shaded part hangs outside the
|  rectangle by one pixel below and to the right.
----------------------------------------------------------------------*/
void ShadedRect( Rect *theRect )
{
	PenNormal();
	EraseRect( theRect );
	FrameRect( theRect );
	
	MoveTo( theRect->left+2,	theRect->bottom );
	LineTo( theRect->right,	theRect->bottom );
	LineTo( theRect->right,	theRect->top+2 );
}

/*----------------------------------------------------------------------
|  Draw the drop-down triangle that indicates that this shaded
|  rectangle is a drop-down (/popup) menu.
----------------------------------------------------------------------*/
void DropDownTriangle( Rect *theRect )
{
	RgnHandle	triRgn;
	
	PenNormal();
	triRgn = NewRgn();
	OpenRgn();
	MoveTo( theRect->right - 2, theRect->top + 3 );
	LineTo( theRect->right - 18, theRect->top + 3 );
	LineTo( theRect->right - 10, theRect->bottom - 4 );
	LineTo( theRect->right - 2, theRect->top + 3 );
	CloseRgn(triRgn);
	InvertRgn(triRgn);
	DisposeRgn(triRgn);
}

/*----------------------------------------------------------------------
|  Draw the pop-up menu icon in its non-popped-up state
----------------------------------------------------------------------*/
pascal void DrawFormatPopup( DialogPtr dlog, short item )
{
	FFormList	*formats;
	Handle		itemHandle;
	short		type;
	Rect		box;
	Str255		Pstr;
	short		i;
	char		*fileFormatString;
	
	SetPort( dlog );
	/*
	 *  Search through the list of file format strings until
	 *  the current file format is found.
	 */
	fileFormatString = "Unknown Format";
	for(formats = fileFormats;formats->typeID != 0;++formats)
	{
	if( currentFileFormat == formats->typeID )
		fileFormatString = formats->name;
	}
	/*
	 *  Draw the shaded rectangle around the userItem
	 */
	GetDItem(dlog,item,&type,&itemHandle,&box);
	ShadedRect(&box);
	/*
	 *  Draw the currently selected file format
	 */
	InsetRect(&box, 2, 2);
	DropDownTriangle(&box);
	box.left += 12;
	box.bottom -= 7;
	box.right -= (box.bottom - box.top) + 12;
	TextFont( systemFont );
	TextSize( 12 );
	TextMode( srcOr );
	TextBox( fileFormatString, strlen(fileFormatString), &box, teJustLeft);
}

/*-------------------------------------------------------------------
|  Bring up a popup menu to select the file format to use when
|  saving games.
-------------------------------------------------------------------*/
void FileFormatPopup( DialogPtr dlog, short item )
{
	FFormList	*formats;
	MenuHandle	popup;
	Handle		itemHandle;
	Str255		Pstr;
	Point		where;
	Rect		box,
			eraseBox,
			textRect;
	short		initialSelection = 1,
			nMenu = 0,
			choice,
			type,
			i;
	
	/*
	 *  Find out what the current file format selection is
	 */
	for(i=0,formats = fileFormats;formats->typeID != 0;++i,++formats)
	{
	if( currentFileFormat == formats->typeID )
		initialSelection = i + 1;
	}
	/*
	 *  Create a popup menu...
	 */
	popup = NewMenu( popupMenuID, "\pFile Format Popup");
	InsertMenu(popup, -1);
	/*  
	 *  Add all applicable file formats to the menu
	 */
	for(formats = fileFormats;formats->typeID != 0;++formats)
	{
	CtoPcpy( Pstr, formats->name );
	InsMenuItem(popup, Pstr, ++nMenu);
	}
	CheckItem(popup,initialSelection,true);
	/*
	 *  Show the menu to the user.
	 */
	GetDItem(dlog,item,&type,&itemHandle,&box);
	SetRect(&eraseBox, box.left, box.top, box.right + 1, box.bottom + 1 );
	where.h = box.left;
	where.v = box.top;
	LocalToGlobal( &where );
	GetDItem(dlog,item - 1,&type,&itemHandle,&textRect);
	InvertRect(&textRect);
	EraseRect(&eraseBox);
	choice = PopUpMenuSelect(popup, where.v + 1, where.h + 1, initialSelection) - 1;
	/*
	 *  If the user made a new selection, remember what it was and
	 *  redraw the popup user item in its non-popped state
	 */
	if( (choice >= 0) && (choice < nMenu) )
	{
	for(formats = fileFormats;choice;--choice)
		++formats;
	currentFileFormat = formats->typeID;
	}
	InvertRect(&textRect);
	InvalRect(&eraseBox);
	DeleteMenu( popupMenuID );
	DisposeMenu( popup );
}

/*-------------------------------------------------------------------
|  DLogHook for SFPopupPutFile
-------------------------------------------------------------------*/
pascal short SFPPutHook( short item, DialogPtr dlog )
{
	short		type;
	Handle		itemHandle;
	Rect		box;
	
	/*
	 *  Apparantly, SFPPutFileter returns an item # of -1 to
	 *  give the filterProc a chance to initialize the
	 *  dialog items.
	 *
	 *  We take this opportunity to set the item handle of
	 *  our popup useritem to a pointer to a procedure that
	 *  draws the popup item in its non-popped state.
	 */
	if( item == -1 )
	{
		GetDItem(dlog,putPopupItem,&type,&item,&box);
		SetDItem(dlog,putPopupItem,type,(Handle)DrawFormatPopup,&box);
	}
	/*
	 *  If the user clicks on the file format popup menu item,
	 *  then we call our routine to handle the popup menu
	 */
	if( item == putPopupItem )
	{
		FileFormatPopup( dlog, item );
	}
	return( item );
}

/*----------------------------------------------------------------------
|  Momentarily hilite a dialog button
|
|  The curvature of the InvertRoundRect may be wrong for large buttons.
----------------------------------------------------------------------*/
void FlashDlogItem( DialogPtr dlog, short itemNum )
{
	Handle	item;
	short	itemType;
	Rect	iRect;
	long	ticky;
	
	GetDItem(dlog,itemNum,&itemType,&item,&iRect);
	InsetRect(&iRect,1,1);
	InvertRoundRect( &iRect, 8, 8 );
	ticky = TickCount() + 10;
	while( ticky > TickCount() );
	InvertRoundRect( &iRect, 8, 8 );
}

/*----------------------------------------------------------------------
|  This filter, when passed to ModalDialog, will allow the user to
|  cut, copy and paste by pressing command-X, command-C or command-V,
|  respectively.  It also checks if the cursor is over an editText
|  item, and if so, the cursor is changed into an IBeam.
|
|  This procedure may be used as the filterProc of any dialog that
|  uses ModelDialog().  Note that this filter calls FlashDlogItem(),
|  above.  In all other respects, it is completely self-contained.
----------------------------------------------------------------------*/
pascal Boolean CutPasteFilter( DialogPtr dlog, EventRecord *event, short *item )
{
	DialogRecord	*dp = (DialogRecord *)dlog;
	TEHandle	te;
	Str255		Pstr;
	long		num;
	char		key,
			code;
	short		itemType,
			itemNum;
	Handle		itemHandle;
	Point		mouse;
	Rect		tRect;
	
	te = dp->textH;
	/*
	 *  Adjust the cursor to an iBeam or Arrow as appropriate
	 */	
	GetMouse( &mouse );
	itemNum = FindDItem(dlog,mouse) + 1;
	if( itemNum > 0 )
		GetDItem(dlog,itemNum,&itemType,&itemHandle,&tRect);
	else
		itemType = statText;
	if( itemType == editText )
		SetCursor(*GetCursor(iBeamCursor));
	else
		InitCursor();
	
	/*
	 *  We only wish to filter keyDown events.
	 */
	if( (event->what == keyDown) || (event->what == autoKey) )
	{
		key  = (event->message & charCodeMask);
		code = (event->message &  keyCodeMask) >> 8;
		/*
		 *  If F2, F3 or F4 (cut, copy or paste) are pressed,
		 *  forge a Command-X, C or V and handle the appropriate
		 *  command below.
		 */
		switch( code )
		{
		case 120:
			event->modifiers |= cmdKey;
			key = 'x';
			break;
		case  99:	
			event->modifiers |= cmdKey;
			key = 'c';
			break;
		case 118:	
			event->modifiers |= cmdKey;
			key = 'v';
			break;
		}
		/*
		 *  Is the command key down?
		 */
		if( event->modifiers & cmdKey )
		{
			switch( key )
			{
				/*
				 *  Command-X, C and V are cut, copy and paste.
				 */
				case 'x':
				case 'c':
					ZeroScrap();
					if( key == 'x' )
						DlgCut( dlog );
					else
						DlgCopy( dlog );
					if( TEToScrap() != noErr )
						SysBeep(120);
					return(true);			
				case 'v':
					if( TEFromScrap() == noErr )
						DlgPaste( dlog );
					else
						SysBeep(0);
					return(true);
				/*
				 *  Command-. cancels
				 */
				case '.':
					*item = 2;
					FlashDlogItem( dlog, 2 );
					return(true);
			}
			/*
			 *  All other command-key combinations do nothing
			 */
			event->what = nullEvent;
			return(false);
		}
		/*
		 *  If RETURN or ENTER was pressed, exit with itemHit = 1.
		 */
		if( (key == 13) || (key == 3) )
		{
			*item = 1;
			FlashDlogItem( dlog, 1 );
			return(true);
		}
		/*
		 *  Swallow non-printing characters
		 */
		if( (key < ' ') && (key != 8) && (key != 9) )
		{
			event->what = nullEvent;
			return(false);
		}
	}
	return(false);
}

/*-------------------------------------------------------------------
|  Put up an SFPutFile dialog box that includes a popup menu
|  for selecting the format of the file to save.
|
|  SFPopupPutFile returns the file type of the format selected
-------------------------------------------------------------------*/
OSType SFPopupPutFile(	Point where, Str255 prompt, Str255 originalName,
			SFReply *reply, OSType defaultType, FFormList *formats )
{	
	/*
	 *  Set the global 'currentFileFormat' to the default format
	 *  to initially display in the popup.
	 */
	currentFileFormat = defaultType;
	fileFormats = formats;
	/*
	 *  Call SFPPutFile to display the "put file" dialg box that has
	 *  the popup useritem.
	 *
	 *  New item #9  = Static text ("File format:")
	 *  New item #10 = User item defining the bounding rect of the popup
	 */
	SFPPutFile(	where, prompt, originalName, SFPPutHook, reply,
			putFileID, CutPasteFilter );
	return(currentFileFormat);
}

/*-------------------------------------------------------------------
|  The following BINHEX file contains the DLOG and DITL resources
|  for the SFPopupPutFile dialog I created.

(This file must be converted with BinHex 4.0)
:%e0'8'p`GA"3GA4'D@aP,R*cFQ-!FR0bBe*6483"!!!!!!!!!!*2NS`!!!!!!3!
!!!(9!!!!e3!!!(T1ZJ!56VVhb%TYj%"R"&0Yj%"1G8kklR3JEH@-X+J!%K064P"
[F(9`8(9d4QPXC5jbFh*M!J!!!(*cFQ058d9%!!"bFh*M8P0&4!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!+-d"p8!!!!!!!!#6dkY$M*1G8MR!"JSEH@-B!)S9!`X!!3
!%@Ef5'`!'NkY!+TBMh!!%#hNDJa!!$aA`%3!5)!E3-j3-#hPDP9!'d$NDN(Yj'S
V5-hfF!!3,H4U3HhNDNK!3N")30(!*NK`!"!6$%!!D'F+F!!3%`a!!%KQ&R!!%#[
rr`a!!#jQ#Nkk!')!!!!9!!!!!!$J!6!!!3!!!!!!!!!!$jm!!!!!Z!!*!!!!!!#
%!0S!PJ%J"!46BACP!!!!!!#H!0S!X!%J"!C$B@jMC@`!!!!!!)J!$J#B!-@)#&0
KGQ8JBA-k!!!!!!!G!-B!-3%ZJ!!!!!!!!$J!fJ"+!5!%"89UC@0d"J!!!!!!8J$
D!'3")!3&4(*TGQAD!!!!!!#G!"%!V3$#%!!!!!!!!"d!$J"r!-@!!!!!!!!!`!!
3!0!!C!J-4QPXC5"'Eh*YBA3k!!!!!!#r!'3!dJ%1!!!!!!%!!!!"e3!!!08!!!"
k!#XLh!6)!!!!(!"'!!&%6%p(!!!!%N4*9%`!!!!H$jm!!!!!!!!!+b&m$jm!'L!
!!"N!+b$J'9"[EL"1G@YT*h-J8dC3GA4'D@aP)%4-6dFC8'pZ)%jeDfNRFb"64P"
eG%CTE'8J4%P86!JA:

-------------------------------------------------------------------*/

keith@Apple.COM (Keith Rollin) (10/08/90)

In article <45421@apple.Apple.COM> greggor@apple.com (Greg L. Anderson) writes:
>
>The previously posted sample code helped a little bit (actually, it helped
>a lot -- I didn't see the "item = -1" initialization event documented
>in Inside Macintosh anywhere.  References, anyone?), but it was uncommented,
>program-specific and hard to read.

Technote #47, page 3.

-- 
------------------------------------------------------------------------------
Keith Rollin  ---  Apple Computer, Inc.  ---  Developer Technical Support
INTERNET: keith@apple.com
    UUCP: {decwrl, hoptoad, nsc, sun, amdahl}!apple!keith
"Argue for your Apple, and sure enough, it's yours" - Keith Rollin, Contusions