Chapter 15 Articles & Notes


  1. Macintosh One-Liners
    1. The Main Loop and Events
    2. Menus
    3. Resources
    4. Windows, Alerts, and Dialogs
    5. Drawing
    6. Interrupts and VBL Tasks
    7. Files
    8. Handles and Pointers
    9. General
    10. Controversy Corner (Don't shoot me; I'm just the messenger.)
  2. Scheme to Manage a "Windows" menu
  3. How to write an INIT in Pascal
  4. How do you play Asynchronous Sound?
  5. Default 2.1 CDEF
  6. ToolBox Gotchas
  7. BitMap Rotation in C (and support routines)
  8. INIT Skeleton Code
  9. New Volume Scanning Algorithm

Scheme to Manage a "Windows" menu

by Ben Cranston

Back in August of 1989 there was a discussion here on methods for implementing a "Windows" menu, one with an entry for each window currently displayed by the application. The act of selection would bring that window to the fore, etc.

David Phillip Oster asked: "what happens if you have multiple files open, all with the same name?" and then suggested "perhaps the simplest solution is to just use the item position in the windows menu, rather than the title, to determine which window the user wishes to select -- a problem with this is that you can get identical menu items, but at least the program can distinguish among them, if not the user."

This posting describes a simple scheme to implement this paradigm. Please feel free to use your "junk" key if you are not interested.

The scheme comprises two parts:

1. A linked list of the windows, sorted by the window names, is kept from a

global cell through a window pointer variable in each window data area.

Note: this list is in addition to the Window Manager's list.

2. A small integer in the window data area holds the index in the menu

of that window's item. I called it MRefNum ("Menu Reference Number),

for lack of a better name.

The management algorithms are:

When a new window is created, the proper place in the linked list must be found. We scan the linked list until we find either the end of the list or a window whose name is lexically greater than that of our new window. We are then interested in the item BEFORE that position which is either the root or a window whose name is lexically less than the new name.

The MRefNum of this preceding entry determines the position in the menu that the new window's name is inserted. It is also incremented and becomes the MRefNum of the new window. The new window is inserted into the chain at this point, and all succeeding windows in the chain have their MRefNum's incremented, corresponding to the fact that their menu entries were pushed down by the insert of the new window name.

If the previous item was the root, the new MRefNum becomes 1, and the same things are done.

When a window is destroyed, the menu item corresponding to its MRefNum is deleted, the window is removed from the chain, and all succeeding windows in the chain have their MRefNum's decremented.

I did have an algorithm for changing the name, which broke down into two possibilities corresponding to moving a name UP in the chain (incrementing the MRefNums between the new and old position) or moving a name DOWN in the chain (decrementing the MRefNums between the old and new position) but in the actual program I deleted and re-inserted the window :-)

Getting from a window to it's menu item is easy, as the MRefNum designates the appropriate menu index. Getting from a menu item to the window is just searching for the appropriate MRefNum value.

Here's the code for creating a window:

/* Insert a window into the proper point in the window chain.
 * Also makes appropriate entry in the window menu.
 */



InsertWind(RWPtr newwind)
{
    RWPtr   lastw = nil;
    RWPtr   nextw = AFWind;  /* the root */
    short   refn;

    while ( (nil!=nextw) && (0<CompHand(newwind->wname,nextw->wname)) ) {
	lastw = nextw;
	nextw = nextw->next;
    }

    refn = (nil!=lastw)?lastw->mrefn:0;
    newwind->mrefn = refn+1;
    newwind->next = nextw;

    if (lastw!=nil)
	lastw->next = newwind;
    else
	AFWind = newwind;

    InsMenuItem(WMenu,"\pa",WMBASE+refn);
    SetItem(WMenu,WMBASE+newwind->mrefn,*newwind->wname);

    while (nil != nextw) {
	nextw->mrefn++;
	nextw = nextw->next;
    }

}

Here's the code for destroying the window:

/* This routine removes a window from the window chain.
 * It also removes the appropriate window menu entry.
 */

RemoveWind(RWPtr mortwind)
{
    RWPtr   lastw = nil;
    RWPtr   nextw = AFWind;  /* the root */

    while ( (nil!=nextw) && (nextw!=mortwind) ) {
	lastw = nextw;
	nextw = nextw->next;
    }

    if (nil!=nextw) {
	if (nil!=lastw)
	    lastw->next = nextw->next;
	else
	    AFWind = nextw->next;
	DelMenuItem(WMenu,WMBASE+mortwind->mrefn);
	while (nil!=nextw) {
	    nextw->mrefn--;
	    nextw = nextw->next;
	}
    } else
	SysBeep(30);
}

Here's some code (case WIMENU) that goes from the menu item to the window.

/* Process selection from menu.
 * Apple menu:
 * 	Note: MUST do DA stuff so MF can switch from Apple menu...
 * 	Note: no "about" box is implemented.
 * FILE menu:
 *	...
 * SHOW menu:
 *	Flip view bits or change displayed resource type.
 * WIND menu:
 *	Iconize/deiconize or bring window to front.
 */

DoMenu(int menuparam)
{
    char       daname[256];
    int        menuitem = LoWord(menuparam);
    WindowPtr  windp = FrontWindow();
    RWPtr      mywind = windp;
    short      wkind = WindowKind(windp);

    switch ( HiWord(menuparam) ) {
    case APMENU:
	if ( menuitem == 1 )
	    SysBeep(30);
	else {
	    GetItem(AMenu,menuitem,daname);
	    OpenDeskAcc(daname);
	    AdjustMenus();
	}
	break;
    case EDMENU:
	SystemEdit(menuitem-1);
	break;
    case FIMENU:
	switch (menuitem) {
	case FMOPEN:
	    OpenWindow();
	    break;
	case FMCLOS:
	    if (wkind < 0)
		CloseDeskAcc(wkind);
	    else
		KillWindow(windp);
	    break;
	case FMPSET:
	    PrintSetup(windp);
	    break;
	case FMPRNT:
	    PrintWindow(windp);
	    break;
	case FMQUIT:
	    ExitToShell();
	}
	break;
    case SHMENU:
	if (nil != windp) {
	    if (menuitem >= SMBASE) {
		OpenType = menuitem-SMBASE;
		SetWindType(windp,OpenType);
	    } else switch (menuitem) {
	    case SMRNUM:
		mywind->wview.show.rnum = mywind->wview.show.rnum ^ 1;
		break;
	    case SMRNAME:
		mywind->wview.show.rname = mywind->wview.show.rname ^ 1;
		break;
	    case SMMASK:
		mywind->wview.show.mask = mywind->wview.show.mask ^ 1;
	    }
	    PostWindowSize(mywind);
	    SetPort(windp);
	    InvalRect(&windp->portRect);
	    AdjustMenus();
	}
	break;
    case WIMENU:
	if (menuitem<WMBASE) {
	    if (nil != windp) {
		switch (menuitem) {
		case WMICZE:
		    if (mywind->wview.show.icize)
			DeIconizeWindow(mywind);
		    else
			IconizeWindow(mywind);
		}
		SetPort(windp);
		InvalRect(&windp->portRect);
		AdjustMenus();
	    }
	} else {
	    for ( mywind=AFWind ; mywind!=nil ; mywind=mywind->next)
		if (WMBASE+mywind->mrefn == menuitem)
		    SelectWindow( (WindowPtr) mywind);
	}

    }
    HiliteMenu(0);
}

Here's some code (last for loop in this proc) that gets from window to menu:

/* Set checkmarks of view and window menus according to mode of front window.
 * There are three cases:
 * If FrontWindow() is nil then there are no windows open (degenerate case).
 * If FrontWindow() is not nil but windowKind is negative then the window is
 *  a Desk Accessory opened by (single) Finder or a DA opened by MultiFinder
 *  within our application heap (option launch).  In this case we want to
 *  make the EDIT menu active but not any of our own menus, since the window
 *  will not have the data items our code assumes are there.
 * If windowKind is positive we assume it is one of our own normal windows,
 *  since we do not have any modeless dialogs.
 */

AdjustMenus()
{
    WindowPtr  windp = FrontWindow();
    RWPtr      mywind = windp;
    short      indx;

    if (nil == windp) {
	DisableItem(FMenu,FMCLOS);
	DisableItem(FMenu,FMPSET);
	DisableItem(FMenu,FMPRNT);
	DisableItem(EMenu,0);
	DisableItem(SMenu,0);
	CheckItem(SMenu,SMRNUM,false);
	CheckItem(SMenu,SMRNAME,false);
	CheckItem(SMenu,SMMASK,false);
	for (indx=0 ; indx<NRType ; indx++)
	    CheckItem(SMenu,SMBASE+indx,false);
	DisableItem(WMenu,0);
	CheckItem(WMenu,WMICZE,false);
    } else if ( WindowKind(windp) < 0 ) {
	EnableItem(FMenu,FMCLOS);
	DisableItem(FMenu,FMPSET);
	DisableItem(FMenu,FMPRNT);
	EnableItem(EMenu,0);
	DisableItem(SMenu,0);
	CheckItem(SMenu,SMRNUM,false);
	CheckItem(SMenu,SMRNAME,false);
	CheckItem(SMenu,SMMASK,false);
	for (indx=0 ; indx<NRType ; indx++)
	    CheckItem(SMenu,SMBASE+indx,false);
	DisableItem(WMenu,0);
	CheckItem(WMenu,WMICZE,false);
    } else {
	EnableItem(FMenu,FMCLOS);
	EnableItem(FMenu,FMPSET);
	EnableItem(FMenu,FMPRNT);
	DisableItem(EMenu,0);
	EnableItem(SMenu,0);
	CheckItem(SMenu,SMRNUM,mywind->wview.show.rnum);
	CheckItem(SMenu,SMRNAME,mywind->wview.show.rname);
	CheckItem(SMenu,SMMASK,mywind->wview.show.mask);
	for (indx=0 ; indx<NRType ; indx++)
	    CheckItem(SMenu,SMBASE+indx,indx==mywind->wdata.dtype);
	EnableItem(WMenu,0);
	CheckItem(WMenu,WMICZE,mywind->wview.show.icize);
    }

    for ( mywind=AFWind ; mywind!=nil ; mywind=mywind->next)
	CheckItem(WMenu,WMBASE+mywind->mrefn,( (RWPtr) windp == mywind ));
	
    DrawMenuBar();
}

These are subroutines used by the above:

/* Compare string handles
 */

CompHand(Handle s1,Handle s2)
{
    int ans;

    HLock(s1);
    HLock(s2);
    ans = IUCompString(*s1,*s2);
    HUnlock(s1);
    HUnlock(s2);
    return(ans);
}

/* Get the windowKind field from the window record.  This is used to
 * decide if a window belongs to the program, or if it is a desk
 * accessory called either from the old Finder environment or from
 * the MultiFinder environment with the option key down.
 */

WindowKind(WindowPtr windp)
{
    int wkind = 0;

    if (nil != windp)
	wkind = ((WindowPeek) windp)->windowKind;
    return(wkind);
}

Actually, it occurs to me that there is no need to explicitly store the MRefNum at all, as it should be identical to the position of that window in the list (that is, reading the list should return 1, 2, 3, etc). Extension to remove the MRefNum cell is left as an exercise to the reader...


How to write an INIT in Pascal

by Matthew Xavier Mora

This started out to be an "is it possible to write an INIT in PASCAL?" project to see if I could do It. The answer is yes and no. It can be done but you will need either some inline assembly or some assembly glue code. The INIT I wrote is a jGNEfilter that checks to see if the user hit one of the extended keyboard function keys. The unit "newJGNEFilter" is not real working code. It is only a framework of what you need to do. I myself have not finished the code yet so I am not sure if it will be the way I go. When my INIT is finished I will update this article to include full working code.

{This unit is the code that will install a jGNEFilter patch.}
{Written by Matthew Xavier Mora}

unit install;
interface
	procedure main;

implementation
	procedure ShowINIT (iconID, moveX: Integer);
	EXTERNAL;
	procedure main;
		var
			newjgne: Handle;
			addr: longint;
			JGNE: ptr;
			jgneptr: ^ptr;
	begin
		SetZone(SystemZone);
		newjgne := Get1Resource('CODE', 128);
		if newjgne <> nil then
			begin
				DetachResource(newjgne);
				HLock(newjgne);
				JGNE := pointer($29A);
				BlockMove(JGNE,ptr(ord(newjgne^)+10),4);{move jgne address into header of newcode}
				jgneptr := pointer($29A);
				jgneptr^ := pointer(newjgne^);
				ShowINIT(128, -1);
			end;
	end;
end.

unit newJGNEFilter;

interface

	procedure main;

implementation
	function GetA1 (dummy: longint): longint;
	external;
	function GetD0 (dummy: longint): integer;
	external;
	procedure Setresult (dummy: integer);
	external;
	procedure DoJsr (addr: ProcPtr);
	inline
		$205F, $4E90;

	procedure DoJmp (addr: ProcPtr);
	inline
		$4CDF, $1CE0, $4E5E, $205F, $4ED0;
				{MOVEM.L    (A7)+,D5-D7/A2-A4}
				{UNLK       A6}
				{MOVEA.L    (A7)+,A0}
				{JMP        (A0)}



	procedure main;
	
		var

			addr, oldjgne: procptr;
			dummy: integer;
			Eventmessage: longint;
			event: eventrecord;
			hasevent: boolean;
			Evntptr: ^eventrecord;
	begin
		hasevent := Boolean(GetD0(0));	   {D0 contains flag of gne}
		if hasevent then
			begin
				Evntptr := pointer(geta1(0));   {A0 contains a pointer to the eventrecord}
				Eventmessage := Evntptr^.message;
				{do whatever you wish }
				{if you hande the event your self then set D0 to false}
				{ SetD0(false);}
			end;
		oldjgne := procptr(ord(@main) - 6);     {get address that was stored into header}
		addr := procptr(oldjgne);
		if addr <> nil then
			DoJmp(addr);				    {jmp to address stored in header}
	end;


How do you play Asynchronous Sound?

by Larry Rosenstein

[Asynchronous Sound Code]

Enclosed is the source for an MPW Pascal unit that shows how to play asynchronous sounds with the Sound Manager. I have tried this unit only on System 6.0.2; supposedly there are bugs in earlier versions of the Sound Manager. This unit also doesn't check for the existence of the Sound Manager, I assume that you do this at a higher level.

I used this in a simple MacApp program that will open any file and allow you to play any snd resource in the file ansynchronously. (I started this with the idea of allowing copy & paste, but haven't gotten that far yet. If there is interest, I can post that program.)

Larry Rosenstein, Object Specialist


---
(*
A simple unit that demonstrates how to produce asynchronous sound with
the Macintosh Sound Manager.  Although I am pretty confident about this code,
I don't guarantee that this code demonstrates the correct way to do things.  It
does seem to work reliably.

This unit doesn't solve any of the tricky issues about using the Sound Manager.
Primarily, sound channels should be disposed of as soon as they are no longer
needed.  This code does just that, but it doesn't prevent your program or a
background program from trying to make sound.

Changes:

2/16/89  Lock the sound resource; state restored in call back
gSoundPlaying is the actual handle.

Larry Rosenstein
Apple Computer, Inc.
lsr@Apple.COM

Copyright 1988-1989 Apple Computer Inc.  All Rights Reserved.
*)

UNIT UAsynchSnd;

INTERFACE

USES
MemTypes, Quickdraw, OSIntf, ToolIntf;

{ call this before any other routine }
PROCEDURE InitUAsynchSnd;

{ returns TRUE if an asynchronous sound is playing }
FUNCTION  IsSoundPlaying: BOOLEAN;

{ equivalent to SndPlay, but does it asynchronously; if you call this
while another sound is playing, the first one will be stopped }
FUNCTION  ASynchSndPlay(sndHandle: Handle): OSErr;

{ stop the sound from playing; may be called even if no sound is
currently playing }
PROCEDURE StopAsynchSound;

{ should be called when your program exits }
PROCEDURE TerminateUAsynchSnd;

IMPLEMENTATION

VAR
gSoundPlaying:          Handle;
gSoundState:            SignedByte;
gSndChannel:            SndChannelPtr;

PROCEDURE ChanCallBack(chan: SndChannelPtr; cmd: SndCommand); FORWARD;
FUNCTION  GetA5: LONGINT; INLINE $2E8D; {MOVE.L A5,(A7)}
FUNCTION  LoadA5(newA5: LONGINT): LONGINT;
                                        INLINE $2F4D,$0004,$2A5F;

(********************)

PROCEDURE InitUAsynchSnd;
BEGIN
gSndChannel := NIL;
gSoundPlaying := NIL;
END;

FUNCTION  ASynchSndPlay(sndHandle: Handle): OSErr;
VAR     err:                    OSErr;
aCommand:               SndCommand;

BEGIN
StopAsynchSound;        { kill the current sound & channel }

err := noErr; { default value }

{ gSndChannel should be NIL now }
err := SndNewChannel(gSndChannel, 0, 0, @ChanCallBack);
{ We don't specify a synthesizer, since we are assuming that
the snd resource specifies one.  For example, the
standard Clink-Klank snd resource doesn't use the 
sampled synthesizer. }

gSoundState := HGetState(sndHandle);
MoveHHi(sndHandle);
HLock(sndHandle);
gSoundPlaying := sndHandle;

IF err = noErr THEN
err := SndPlay(gSndChannel, sndHandle, TRUE);

WITH aCommand DO BEGIN
cmd := callBackCmd;
param1 := 0;
param2 := GetA5;
END;
IF err = noErr THEN
err := SndDoCommand(gSndChannel, aCommand, FALSE);

IF err <> noErr THEN
StopAsynchSound;                { flush channel; unlock sound }

AsynchSndPlay := err;
END;

PROCEDURE ChanCallBack(chan: SndChannelPtr; cmd: SndCommand);
VAR     oldA5:  LONGINT;
BEGIN
oldA5 := LoadA5(cmd.param2);    { get the application's A5 and set it }

HSetState(gSoundPlaying, gSoundState);
gSoundPlaying := NIL;

oldA5 := LoadA5(oldA5);                 { restore old A5 }
END;

FUNCTION  IsSoundPlaying: BOOLEAN;
BEGIN
IsSoundPlaying := gSoundPlaying <> NIL;
END;

PROCEDURE StopAsynchSound;
BEGIN
IF gSndChannel <> NIL THEN BEGIN
IF SndDisposeChannel(gSndChannel, TRUE) = noErr THEN { nothing };
gSndChannel := NIL;
END;
IF gSoundPlaying <> NIL THEN
BEGIN
HSetState(gSoundPlaying, gSoundState);
gSoundPlaying := NIL;
END;
END;

PROCEDURE TerminateUAsynchSnd;
BEGIN
StopAsynchSound;
END;

END.


Default 2.1 CDEF

By Lloyd Lim

(c)1990 Lim Unlimited -- All Rights Reserved

The Default CDEF is a simple aid for Macintosh programmers that draws default button outlines for any size buttons, in the proper color, in your application and in ResEdit dialog and alert templates. Push buttons, check boxes, and radio buttons can also be drawn using the window's font.

Instructions

Default is a control definition function that you can copy and paste into your application. The Default CDEF enhances the System file's standard control definition 0. If the last character of a button's title is an `@' (an at or an each symbol), the button is drawn with an outline indicating that it is the default button. The trailing `@' is not drawn with the title.

If the button is inactive, the outline is grayed out. When Color QuickDraw is available, outlines are drawn in the same color as the button's frame. If the Default CDEF is in the same file as your application's DITL resources, ResEdit will display default button outlines drawn by Default. You do not need to write any code to use Default.

A dialog's default button can be changed at any time in your application simply by changing the appropriate button titles. However, you, the programmer, are responsible for making sure there is only one default button. If you are not using a filterProc, Default makes sure that pressing the Return key or Enter key is the same as clicking in the default button. The default button is also highlighted when the Return key or Enter key is pressed. If you are using your own filterProc, you must perform these tasks.

If the last character of a control's title is an `[[florin]]', the control is drawn using the window's current font. Push buttons, check boxes, and radio buttons will look the same as if you used a CNTL resource with the useWFont variation code. This feature is especially useful for CDEVs. If you need a default button that uses the window's font, end the button's title with `[[florin]]@'.

Compatibility

Default should work on any model of Macintosh with any System version. Default is 32-bit clean. Except for drawing the outline, Default lets the System file's CDEF 0 do practically everything. Thus, Default's buttons do anything that normal buttons do.

Be warned that Default uses the contrlData field of a ControlRecord (which is okay since it is reserved for CDEFs). If your application does something strange with this field, Default will not work.

When you drag or resize a default button in ResEdit, you may notice some slight flickering. This occurs because ResEdit does not know about the outline so Default forces ResEdit to refresh portions of the window. Default only behaves this way in ResEdit. This does not occur in normal applications. Default now comes in versions with and without the ResEdit updating code. This may be helpful if you are concerned about your application's size.

Bugs

The outline will not update if the outline needs updating and the button does not, and UpdtControl or UpdtDialog is being used to update the controls. This problem does not occur with normal modal dialogs and occurs rarely with modeless dialogs. Unfortunately, there doesn't seem to be a clean solution this problem. If you have this problem, you can call Draw1Control for the default button on every update event or simply use DrawControls or DrawDialog instead.

If you have a default button and you aren't using a filterProc, pressing the Return key or Enter key will click in the default button even if the button is hidden. This is a bug in the Dialog Manager. If you have this problem, you must change the default button to a normal button before you call HideDItem or HideControl.

Please report any other bugs to any of the addresses listed below. Thanks to those programmers who did unusual things with their buttons, reported bugs, and helped make Default more robust.

History

1.0 -- first release version
1.1 -- fixed bug with HideControl and default buttons
1.2 -- updated to support new 32-bit clean CDEF messages
1.3 -- fixed bug with DragControl
1.4 -- fixed bug converting between default buttons and normal buttons;
       improved outlines for unusually sized default buttons
1.5 -- forced ResEdit to refresh default buttons correctly
2.0 -- added automatic Return and Enter support for default buttons;
       added window font feature for push buttons, check boxes, and radio buttons;
       restructured code to reduce size of CDEF
2.1 -- updated to follow Apple's new way of drawing default outlines

Distribution

Default is copyrighted but it is also available free of charge. You may copy and redistribute Default provided that this documentation accompanies any redistributed copies of Default and the Default CDEF is not modified in any way.

You may include Default in any commercial or non-commercial software that you distribute provided that the Default CDEF is not modified in any way and that Lim Unlimited is given a free, fully functional, and fully supported copy of your software. You are not required to include a copyright notice for Default in your software.

The source code to Default is now available on request. You can get a copy via electronic mail or by sending a stamped self-addressed envelope and a disk via postal mail. The source code may not be redistributed without the permission of Lim Unlimited. You may not distribute modified versions of the source code or any software derived from the source code.

Lim Unlimited

Postal: 330 W. Iris, Stockton, CA 95210, U.S.A.

Internet: lim@iris.ucdavis.edu

America Online: LimUnltd

CompuServe: 72647,660

/*
	Default is a CDEF written using THINK C. This CDEF replaces the standard CDEF 0 and
	simply draws a default button outline and calls the standard CDEF 0 to handle
	everything else.

	(c) Lim Unlimited, 9 Jul 1990 - All Rights Reserved
*/



/*
	header files
*/

#include <Color.h>
#include <ColorToolbox.h>
#include <FontMgr.h>



/*
	constants and macros
*/

#define RESEDIT	1		/* whether to compile ResEdit updating code */

#define NIL		((void *) 0)

#define INACTIVE	255

#define STANDARD_CDEF_ID	0

enum {						/* new 32-bit clean messages */
	calcCntlRgn = 10,
	calcThumbRgn
};

#define LO_3_BYTES	0x00FFFFFF

#define SYS_ENVIRONS_VERSION	1

#define DEFAULT_FLAG		'@'
#define USEWFONT_FLAG	'[[florin]]'

#define OUTLINE_THICKNESS	3
#define OUTLINE_INSET		(OUTLINE_THICKNESS + 1)
#define OUTLINE_OUTSET		(-OUTLINE_INSET)
#define CURVE_ADJUSTMENT	2

#define RECT_IN_RECT(r1, r2)													\
	((r1)->top >= (r2)->top && (r1)->left >= (r2)->left &&			\
	 (r1)->bottom <= (r2)->bottom && (r1)->right <= (r2)->right)



/*
	typedefs
*/

typedef struct {
	ControlHandle	itmHndl;
	Rect				itmRect;
	Byte				itmType;
	Byte				itmData[1];
} Item, *ItemPtr;

typedef struct {
	short		dlgMaxIndex;
	Item		item[1];
} ItemList, *ItemListPtr, **ItemListHdl;

typedef struct {
	Handle		standardCDEF;
	unsigned		has128KROMS:1;
	unsigned		hasColorQD:1;
	unsigned		dialogButton:1;
	unsigned		dialogDefault:1;
#if RESEDIT
	unsigned		inResEdit:1;
#endif
} DefaultData, *DefaultDataPtr, **DefaultDataHdl;



/*
	routines
*/

PASCAL long			main						(short, ControlHandle, short, long);



/*
	static routines
*/

static long			CallStandardCDEF		(short, ControlHandle, short, long);
static Boolean		RealHandle				(long, Boolean);
static short		FindItemNum				(DialogPeek, ControlHandle);



/*
	static variables
*/

static unsigned char		Copyright[] = "Default 2.1, (c)1990 Lim Unlimited";



/*
	main draws the default button outline and calls the standard button CDEF.
*/

PASCAL long main(varCode, theControl, message, param)

register short				varCode;
register ControlHandle	theControl;
register short				message;
register long				param;

{
	register DefaultDataHdl		defaultDataHdl;
	unsigned char					*title;
	DialogPeek						theDialog;
	Boolean							isDefault, useWindowFont, dialogDefault;
	short								oldRefNum;
	Handle							standardCDEF;
	SysEnvRec						sysEnvirons;
	Rect								button;
	AuxCtlHndl						auxCtlHdl;
	RGBColor							oldColor, frameColor;
	PenState							penState;
	Pattern							gray;
	RgnHandle						outlineRgn;
	register short					curve;
	register long					result;
#if RESEDIT
	RgnHandle						visRgn, oldVisRgn;
	Rect								visButton, visBounds;
#endif
	
	title = (*theControl)->contrlTitle;
	if (!(varCode & ~useWFont) && title[title[0]] == (unsigned char) DEFAULT_FLAG) {
		isDefault = TRUE;
		--title[0];
	} else {
		isDefault = FALSE;
	}
	if (useWindowFont = (title[title[0]] == (unsigned char) USEWFONT_FLAG)) {
		varCode |= useWFont;
		--title[0];
	}
	theDialog = (DialogPeek) (*theControl)->
;
	
	if (message == initCntl) {
		defaultDataHdl = (DefaultDataHdl) NewHandle(sizeof(DefaultData));
		(*theControl)->contrlData = (Handle) defaultDataHdl;

		oldRefNum = CurResFile();
		UseResFile(0);
		standardCDEF = GetResource('CDEF', STANDARD_CDEF_ID);
		(*defaultDataHdl)->standardCDEF = standardCDEF;
		UseResFile(oldRefNum);
		HNoPurge(standardCDEF);
		
		result = SysEnvirons(SYS_ENVIRONS_VERSION, &sysEnvirons);
		(*defaultDataHdl)->has128KROMS = (sysEnvirons.machineType >= envMachUnknown);
		(*defaultDataHdl)->hasColorQD = sysEnvirons.hasColorQD;
		if (!(varCode & ~useWFont) &&
			 RealHandle((long) theDialog->items, !result) &&
			 RealHandle((long) theDialog->textH, !result) &&
			 theDialog->editField >= -1 &&
			 theDialog->editField <= (*(ItemListHdl) theDialog->items)->dlgMaxIndex) {
			(*defaultDataHdl)->dialogButton = TRUE;
		} else {
			(*defaultDataHdl)->dialogButton = FALSE;
		}
		(*defaultDataHdl)->dialogDefault = FALSE;
		
#if RESEDIT
		/* ResEdit is being used if the control's window has no controls attached */
		if (!theDialog->window.controlList && theDialog == (DialogPeek) FrontWindow()) {
			(*defaultDataHdl)->inResEdit = TRUE;
		} else {
			(*defaultDataHdl)->inResEdit = FALSE;
		}
#endif
		
		result = CallStandardCDEF(varCode, theControl, message, param);
		
	} else {
		defaultDataHdl = (DefaultDataHdl) (*theControl)->contrlData;
		
		/* set default button item number for Dialog Manager */
		if ((*defaultDataHdl)->dialogButton) {
			dialogDefault = (isDefault && !(*theControl)->contrlHilite);
			if (dialogDefault && !(*defaultDataHdl)->dialogDefault) {
				theDialog->aDefItem = FindItemNum(theDialog, theControl);
				if (theDialog->aDefItem) {
					(*defaultDataHdl)->dialogDefault = TRUE;
				}
			} else if (!dialogDefault && (*defaultDataHdl)->dialogDefault) {
				if (theDialog->aDefItem == FindItemNum(theDialog, theControl)) {
					theDialog->aDefItem = 0;
				}
				(*defaultDataHdl)->dialogDefault = FALSE;
			}
		}
	
		if (message == drawCntl) {
			
			result = CallStandardCDEF(varCode, theControl, message, param);
			if (isDefault && (*theControl)->contrlVis) {
#if RESEDIT
				/* portions of the ResEdit window are invalidated under certain
					circumstances so it will update correctly */
				if ((*defaultDataHdl)->inResEdit) {
					visRgn = theDialog->window.port.visRgn;
					visBounds = (*visRgn)->rgnBBox;
					button = (*theControl)->contrlRect;
					if (SectRect(&button, &theDialog->window.port.portRect, &visButton) &&
						 RECT_IN_RECT(&visButton, &visBounds)) {
						InsetRect(&button, OUTLINE_OUTSET, OUTLINE_OUTSET);
						if (SectRect(&button, &theDialog->window.port.portRect, &visButton) &&
							 !RECT_IN_RECT(&visButton, &visBounds)) {
							outlineRgn = NewRgn();
							oldVisRgn = NewRgn();
							CopyRgn(visRgn, outlineRgn);
							CopyRgn(visRgn, oldVisRgn);
							RectRgn(visRgn, &theDialog->window.port.portRect);
							InsetRgn(outlineRgn, OUTLINE_OUTSET, OUTLINE_OUTSET);
							DiffRgn(outlineRgn, oldVisRgn, outlineRgn);
							EraseRgn(outlineRgn);
							CopyRgn(oldVisRgn, visRgn);
							DisposeRgn(oldVisRgn);
							InvalRgn(outlineRgn);
							DisposeRgn(outlineRgn);
						}
					}
				}
#endif

				if ((*defaultDataHdl)->hasColorQD) {
					(void) GetAuxCtl(theControl, &auxCtlHdl);
					GetForeColor(&oldColor);
					if (auxCtlHdl) {
						frameColor = (*(*auxCtlHdl)->acCTable)->ctTable[cFrameColor].rgb;
						RGBForeColor(&frameColor);
					}
				}
				
				GetPenState(&penState);
				PenNormal();
				PenSize(OUTLINE_THICKNESS, OUTLINE_THICKNESS);
				if ((*theControl)->contrlHilite == INACTIVE) {
					*(unsigned long *) &gray[0] = *(unsigned long *) &gray[4] = 0xAA55AA55;
					PenPat(&gray);
				}
				button = (*theControl)->contrlRect;
				InsetRect(&button, OUTLINE_OUTSET, OUTLINE_OUTSET);
				curve = ((button.bottom - button.top) >> 1) + CURVE_ADJUSTMENT;
				FrameRoundRect(&button, curve, curve);
				SetPenState(&penState);
				
				if ((*defaultDataHdl)->hasColorQD) {
					RGBForeColor(&oldColor);
				}
			}
	
		} else if (message == calcCRgns ||
					  message == calcCntlRgn ||
					  message == calcThumbRgn) {
			result = CallStandardCDEF(varCode, theControl, message, param);
			if (isDefault && ((message == calcCRgns && param > 0) ||
									message == calcCntlRgn)) {
				OpenRgn();
				button = (*theControl)->contrlRect;
				InsetRect(&button, OUTLINE_OUTSET, OUTLINE_OUTSET);
				curve = ((button.bottom - button.top) >> 1) + CURVE_ADJUSTMENT;
				FrameRoundRect(&button, curve, curve);
				CloseRgn((RgnHandle) param);
			}
		
		} else if (message == dispCntl) {
			result = CallStandardCDEF(varCode, theControl, message, param);
			
			/* MultiFinder loads and shares system resources in the system heap; make
				standard button CDEF purgeable only if it is in the application heap */
			standardCDEF = (*defaultDataHdl)->standardCDEF;
			if (HandleZone(standardCDEF) == ApplicZone()) {
				HPurge(standardCDEF);
			}
			DisposHandle(defaultDataHdl);
		
#if 0
		/* messages not used by Default but documented for completeness */
		} else if (message == testCntl ||
					  message == posCntl ||
					  message == thumbCntl ||
					  message == dragCntl ||
					  message == autoTrack) {
			result = CallStandardCDEF(varCode, theControl, message, param);
#endif
		
		/* pass along messages which are not used by Default or are currently undefined */
		} else {
			result = CallStandardCDEF(varCode, theControl, message, param);
		}
	}
	
	if (useWindowFont) {
		++(*theControl)->contrlTitle[0];
	}
	if (isDefault) {
		++(*theControl)->contrlTitle[0];
	}
	return(result);
}



/*
	CallStandardCDEF calls the standard button CDEF.
*/

static long CallStandardCDEF(varCode, theControl, message, param)

short				varCode;
ControlHandle	theControl;
short				message;
long				param;

{
	register DefaultDataHdl		defaultDataHdl;
	register Handle				standardCDEF;
	register SignedByte			flags;
	register long					result;
	
	defaultDataHdl = (DefaultDataHdl) (*theControl)->contrlData;
	
	/* load standard button CDEF if it was purged */
	standardCDEF = (*defaultDataHdl)->standardCDEF;
	if (!*standardCDEF) {
		LoadResource(standardCDEF);
		HNoPurge(standardCDEF);
	}
	
	/* save and restore handle state just in case control is reentrant */
	if ((*defaultDataHdl)->has128KROMS) {
		flags = HGetState(standardCDEF);
	} else {
		flags = (long) *standardCDEF >> 24;
	}
	HLock(standardCDEF);
	result = CallPascalL(varCode, theControl, message, param, *standardCDEF);
	if ((*defaultDataHdl)->has128KROMS) {
		HSetState(standardCDEF, flags);
	} else {
		*standardCDEF = (Ptr) (((long) *standardCDEF & LO_3_BYTES) | (flags << 24));
	}
	return(result);
}

/*
	RealHandle returns whether the given address is a handle in the system heap or
	application heap.
*/

static Boolean RealHandle(addr, hasStripAddr)

register long	addr;
Boolean			hasStripAddr;

{
	register Boolean	real;
	register THz		sysZone, applZone, heapZone;
	real = FALSE;
	addr = (hasStripAddr) ? StripAddress(addr) : addr & LO_3_BYTES;
	if (addr && !(addr & 1)) {
		sysZone = SystemZone();
		applZone = ApplicZone();
		if (((addr >= (long) &sysZone->heapData && addr < (long) sysZone->bkLim) ||
			  (addr >= (long) &applZone->heapData && addr < (long) applZone->bkLim)) &&
			 *(long *) addr && !(*(long *) addr & 1)) {
			heapZone = HandleZone(addr);
			if (!MemError() && (heapZone == sysZone || heapZone == applZone)) {
				real = TRUE;
			}
		}
	}
	return(real);
}





/*
	FindItemNum returns the item number of the given control in the given dialog.
*/

static short FindItemNum(theDialog, theControl)

DialogPeek		theDialog;
ControlHandle	theControl;

{
	register short		numItems, itemNum;
	register Ptr		itemPtr;
	
	numItems = (*(ItemListHdl) theDialog->items)->dlgMaxIndex + 1;
	itemPtr = (Ptr) (*(ItemListHdl) theDialog->items)->item;
	for (itemNum = OK; itemNum <= numItems; ++itemNum) {
		if (((ItemPtr) itemPtr)->itmHndl == theControl) return(itemNum);
		itemPtr += sizeof(Item) + (((long) ((ItemPtr) itemPtr)->itmData[0] + 1) & ~1);
	}
	return(0);
}

ToolBox Gotchas

by John Norstad

While developing Disinfectant I ran into a number of "gotchas" that caused me great grief. I thought it would be nice to tell the rest of you about these problems, in the hope that you'll be able to avoid them in your own programs. I've told DTS about most of this stuff.

Gotcha #1. Watch out for PBGetCatInfo calls with TOPS.

The file manager routine PBGetCatInfo uses a parameter block of type CInfoPBRec. Make certain that you pass a pointer to the full parameter block when using MPW C, even if you know in advance that the object is a directory. Don't just allocate and pass a pointer to the DirInfo variant. The DirInfo variant is four bytes shorter than the full union type, and with TOPS the PBGetCatInfo call sets those four bytes at the end. If your parameter block is not big enough you'll trash the stack.

Gotcha #2. Make certain you're in the proper heap zone before calling ReleaseResource.

At the bottom of IM II-26, in the Memory Mangler chapter, is the warning "Be sure, when calling routines that access blocks, that the zone in which the block is located is the current zone." Heed this warning, especially when releasing resources. Bob Hablutzel and I discovered (after hours in Macsbug) that on the 128K ROMs, if you try to release an empty (unloaded) resource in the system heap, and if you neglect to set the current zone to the system zone, then the system will trash the free master pointer list. This is not good, and will almost undoubtedly lead to subsequent bizarre behavior.

Here's the code I use to release a resource:
   curZone = GetZone();
   SetZone(HandleZone(theRez));
   ReleaseResource(theRez);
   SetZone(curZone);

Gotcha #3. Don't believe Inside Macintosh. (HandleZone)

On page IM II-34 we read the following warning in the description of the HandleZone routine: "If handle h is empty (points to a NIL master pointer), HandleZone returns a pointer to the current heap zone." This is false - HandleZone properly returns a pointer to the heap zone that contains the master pointer. See Gotcha #2 above.

Gotcha #4. Don't expect OpenResFile to do sanity checking.

Neither OpenResFile nor OpenRFPerm does any sanity checking of any sort when opening a resource file. If the file is damaged or contains trash it is very possible for the Resource Mangler to bomb or hang inside the OpenResFile or OpenRFPerm call. Often what happens is that it makes a Memory Mangler request for some ridiculously huge block of memory. If you have a GrowZone proc this can cause problems.

To prevent this problem you must write a sanity checker of your own that opens the resource file as a binary file and checks at least the most important structural characteristics of the file. If your sanity check fails you must avoid calling OpenResFile or OpenRFPerm on the file. In Disinfectant I check that the resource map and resource data are within the logical EOF of the file and don't overlap, I check that the resource type list immediately follows the resource map, and I check that the resource name list starts within the logical eof.

DTS tells me that the only way to be completely safe is to do a complete sanity check of the entire resource fork - e.g., rewrite the RezDet MPW tool.

Damaged and trashed resource forks are much more common than you might think.

Gotcha #5. Don't believe Inside Macintosh. (OpenResFile)

In the description of the OpenResFile routine, IM I-115 states "If the resource file is already open, it doesn't make it the current resource file; it simply returns the reference number." This is false. If the resource file is already open, OpenResFile in fact DOES make it the current resource file. OpenRFPerm also has the same behavior, in those cases where OpenRFPerm returns the reference number of the previously opened copy of the file, rather than opening a new access path (see IM IV-17 and TN 185).

Gotcha #6. Watch out for Standard File if you unmount volumes.

The standard file package keeps track of the last volume it used in the low core global SFSaveDisk, which contains the negative of the vol ref num of the last volume used. If your program unmounts this volume and then later calls the standard file package again, it will post an alert saying that "A system error has occurred. Please try again." A simple fix for this problem is to check the vRefNum stored in SFSaveDisk immediately before any calls to standard file. Call PBGetVInfo to see if the volume still exists. If it doesn't, make an indexed call to PBGetVInfo to get the vRefNum of the first volume in the VCB queue, and set SFSaveDisk to the negative of this vRefNum. Also set CurDirStore to fsRtDirID.

Gotcha #7. Don't believe Inside Macintosh.

IM I-116 states that "When calling the CurResFile and HomeResFile routines, described below, be aware that for the system resource file the actual reference number is returned." This is false. CurResFile does indeed return the actual reference number of the system file (2), but HomeResFile in fact returns 0 for system file resources.

Gotcha #8. Don't believe Inside Macintosh.

IM I-126 states "Like the attributes of individual resources, resource file attributes are specified by bits in the low-order byte of a word." This is false. In fact, the resource file attributes are stored in the high-order byte of the word.

Gotcha #9. Directory IDs are longs, not shorts, stupid.

Directory IDs, unlike volume reference numbers and working directory ids, are longs, not shorts. Watch out for this one. It's really easy to declare a dirID to be a short by mistake, and unless you're using Modula-2 you probably won't catch the bug even with extensive beta testing. Don't feel too stupid if you do this - I have it on good authority that ResEdit once had this bug!

Gotcha #10. Always set the ioNamePtr field in file manager param blocks.

See TN 179. Read it. Believe it. Always set ioNamePtr. Set it to nil if you don't care about the name. I made this mistake three times while developing Disinfectant, and all three times it took FOREVER to find the bug. The problem is those silly little arrows in the file manager chapter of IM IV. They all point to the left for ioNamePtr, which usually means that you don't have to set the field before calling the routine.

I hope my experiences help somebody.

John Norstad Academic Computing and Network Services Northwestern University


How do you Hide the menu bar?

by: Earle R. Horton

The code I posted yesterday would cause problems in the unlikelyevent that your program would crash while the Menu Bar was hidden. Specifically, it would replace GrayRgn with a handle to a Region in the application heap, and save the real GrayRgn Handle for restoration later when you restored the Menu Bar. If your program crashed, and the Menu Bar was hidden, then GrayRgn was left pointing to a Region in a defunct application heap, which could cause all sorts of problems for applications which were still running.

This version does not change the Handle, but rather modifies the contents of GrayRgn. If you crash with the Menu Bar hidden, GrayRgn is left pointing to a valid area of storage, at least. There are still problems since the Menu Bar can be left hidden, but these are slightly less severe than leaving a dangling Handle in the system heap.

Earle

{
This unit provides the ability to hide the Menu Bar, and to show
it.  When the Menu Bar is hidden, windows may be placed in the
area normally used for the Menu Bar.

Usage:
        PROCEDURE MBar_Init:
          Used to initialize global variables used here.
          Call once at beginning of application code.
        PROCEDURE MBar_be_Gone;
          Hides the Menu Bar.
        PROCEDURE MBar_Restore;
          Shows the Menu Bar.  Call this whenever you are going
          to be put into the background.  Call before Exit.

The procedures in this unit will probably break under some future
release of the Macintosh Operating System, because they manipulate
the GrayRgn.  They do work under MultiFinder 6.1a2.  The most 
serious warning I can give concerning use of these routines is
that you must never allow your application to be placed into the
background with the Menu Bar hidden.

Possible compatibility problems:

  Modifies lowmem MBarHeight
  Modifies contents of GrayRgn
 
This file is part of Earle R. Horton's private source code library.
Earle R. Horton assumes no responsibility for any damages arising
out of use of this source code for any purpose.  Earle R. Horton
places no restrictions on use of all or any part of this source code,
except that this paragraph may not be altered or removed.

Original Language:
   MPW Pascal, v. 2.0.2
Origination Date:
   March 20, 1989
Modifications:
   April 4, 1989 ERH
     First version changed lowmem GrayRgn to point to a Region in the
application heap while the Menu Bar was hidden.  This version copies the
new Region to GrayRgn.  If the application crashes with the Menu Bar
hidden, GrayRgn no longer points to part of the defunct application
heap.  There are still problems, however, because the Menu Bar is left
hidden and other applications cannot access it. 
}
UNIT MenuBar;
  INTERFACE

    USES
      {$Load PasDump.dump}
      Memtypes, Quickdraw, OSIntf, Script, ToolIntf;

    PROCEDURE MBar_be_Gone;
    PROCEDURE MBar_Restore;
    PROCEDURE MBar_Init;

    PROCEDURE SetMBarHeight(newheight:integer);
    INLINE smPopStack2Word,smMBarHeight;        { move.w        (a7)+,$0BAA }

    VAR
      Real_MBar_Height: integer;        { Copy of lowmem MBarHeight }
      Save_Region:      RgnHandle;      { Copy of GrayRgn }
      Hidden_Flag:      Boolean;        { State info }
      MBar_Rect:        Rect;           { Rect in which MBar is drawn }

  IMPLEMENTATION

    PROCEDURE MBar_Init;
    BEGIN
      Hidden_FLag := false;
      Real_MBar_Height := GetMBarHeight;
      SetRect(MBar_Rect,
              screenBits.bounds.left,
              screenBits.bounds.top,
              screenBits.bounds.right,
              screenBits.bounds.top + Real_MBar_Height);
    END;
{
Make the Menu Bar go away under MultiFinder or UniFinder.
Since this procedure manipulates the Gray Region, future
compatibility is unknown.
}
    PROCEDURE MBar_be_Gone;
    VAR
      MBar_Region:      RgnHandle;
      theWindow:        WindowPeek;
      BEGIN
        IF not Hidden_Flag THEN
          BEGIN
            Hidden_Flag := true;
                                                { Get some Regions to work with}
            Save_Region := NewRgn;
            MBar_Region := NewRgn;
                                              { Set the Menu Bar height to zero}
            SetMBarHeight(0);
 }
            CopyRgn(GetGrayRgn,Save_Region);
               { Fix up GrayRgn to cover the old GrayRgn plus the Menu Bar Rect}
            RectRgn(MBar_Region,MBar_Rect);
            UnionRgn(GetGrayRgn,MBar_Region,GetGrayRgn);
                    { Paint and fix up visRgn for any windows with exposed area}
            theWindow := WindowPeek(FrontWindow);
            PaintOne(theWindow,MBar_Region);
            PaintBehind(theWindow,MBar_Region);
            CalcVis(theWindow);
            CalcVisBehind(theWindow,MBar_Region);
            DisposeRgn(MBar_Region);                          { Clean up, leave}
          END;
      END;
{
Restore the Menu Bar and GrayRgn to normality.
Call when app is put into background.
}
    PROCEDURE MBar_Restore;
    VAR
      theWindow:        WindowPeek;
    BEGIN
    IF Hidden_Flag THEN
      BEGIN
        Hidden_Flag := false;
                           { Restore to original }
        CopyRgn(Save_Region,GetGrayRgn);
                       { Restore Menu Bar height }
        SetMBarHeight(Real_MBar_Height);
                    { Fix up any covered windows }
        RectRgn(Save_Region,MBar_Rect);
        theWindow := WindowPeek(FrontWindow);
        CalcVis(theWindow);
        CalcVisBehind(theWindow,Save_Region);
        DisposeRgn(Save_Region);
                { Draw the Menu, get out of here }
        HiliteMenu(0);
        DrawMenuBar;
      END;
    END;
END.