MultiFinder Icon Fix
Volume Number: | | 6
|
Issue Number: | | 3
|
Column Tag: | | Programmer's Workshop
|
Related Info: Menu Manager OS Utilities Quickdraw Resource Manager
Æeshete--a fix for MultiFinder
By David Dunham, Bellevue, WA
Introduction
Ive enjoyed multitasking on my Macintosh since I discovered in 1984 that the Key Caps DA kept running in a background window. So I should be happy with MultiFinder, right? Well, almost. Besides multitasking, I like my Macintosh because its, well, pretty. From the cute startup icon to the zooming Open command in the Finder, theres an attention to artistic detail. Except in MultiFinder. I had to keep staring at that ugly shrunken representation of an icon at the right of the menu bar.
It was pretty obvious why it was ugly -- QuickDraw was just scaling down a 32*32 icon to 16*16 pixels. Why couldnt MultiFinder plot small icons (SICN resources) instead of shrinking icons? (I dont know -- I did suggest this in my bug reports. Apparently, this will happen in System 7.0.)
Checking for Traps
Obviously I had to do something to make my Macintosh aesthetic again. But what? Presumably MultiFinder was drawing the ugly menu bar icon, or at least patching the Menu Manager to do its dirty work. But patching MultiFinder didnt seem like a good approach. First of all, it would be a lot of work -- Id probably have to disassemble it, and I neither own a disassembler nor enjoy reading undocumented code. Second, there are several versions of MultiFinder. Each time a new one came out, Id have to figure out how to patch it. Perhaps the Menu Manager (as embodied in the MBDF and MDEF defprocs) was responsible for the ugly icons, but these resources also get revised with each System release, and werent good candidates for patching.
My next thought was to patch the _PlotIcon trap, which must be responsible for drawing the shrunken icons. I used a low-level debugger to determine that _PlotIcon was never in fact getting called. Oops. But that was OK, because _CopyBits was called. This wasnt a great surprise, since _PlotIcon would have called _CopyBits in any case. Unfortunately, _CopyBits wasnt a good trap to patch. Its used all over the place to blit (bit block transfer) to the screen, so any overhead I added to it would slow down frequent operations such as drawing scroll bars and radio buttons. Further, its argument is a bitmap, rather than a handle to an icon, which would make it difficult to determine whether or not the bitmap was an application icon. Worse, there might be legitimate cases for plotting ugly shrunken icons -- I wanted my patch to fix MultiFinder, not break other programs.
Finally, it dawned on me. I could plot aesthetic icons by patching _DetachResource. _DetachResource? Yes, because the applications resource file may not be accessible when the menus drawn. If you need resource data, but cant guarantee that the resource will be available, the simplest thing would be to do
/* 1 */
handle = GetResource(type, id);
DetachResource(handle);
and then use the handle some time later (of course, youd have to make sure your resource wasnt purgeable, or call _HNoPurge on the handle). I hauled out the debugger again, and sure enough, MultiFinder was calling _DetachResource on ICN# resources.
This would be just the right trap to patch -- its not called too frequently, and probably never in time-critical loops. And its argument is a handle, which I could easily call _GetResInfo on to determine if it was an ICN#.
The Patch
So now that I knew how to identify where MultiFinder got the icons for the menu, I had to replace them with SICNs. I could have replaced them with different ICN#s, but ResEdit has a graphical SICN editor which I like using. And SICNs are just bitmap data, the 16*16 equivalent of the 32*32 ICN#s.
The patch is simple (see listing 1): whenever someone calls _DetachResource on an ICN#, replace the data with a doubled SICN; when its plotted, itll be shrunk down to 50% and look like a SICN again. Just which SICN I determine by getting the current applications signature (I find its name with _PBGetFCBInfo, then the signature with _PBHGetFInfo), then looking it up in a list (the CRE# resource, discussed below). If I find the signature, and the ICN# is for the application, I replace the ICN# data. Note that I replace only the icon data. I dont touch the mask, because its not used in drawing menu icons.
In an attempt to prevent replacing ICN#s which are being drawn by programs other than their owner, the test in the beginning makes sure the ICN# belongs to the current application. The ICN# used by Finder comes from the System file, so it doesnt belong to any application. It is, however, identifiable as ICN# 3.
In the original version of the patch, I got the SICN and doubled it by _CopyBitsing it right into the ICN#.
/* 2 */
typedef struct {
OSType creator;
word rsrcID;
} SIGNATURE;
/*
AESTHETIZE -- Change an ICN# resource (so its aesthetic when shrunken)
*/
void aesthetize(handle, sicn, creators)
Handle handle, sicn; SIGNATURE **creators;
{
word sicnNum = -1;
word id;
OSType type;
Str255 name;
FCBPBRec fcb;
HParamBlockRec pb;
OSErr error;
register word i;
GetResInfo(handle, &id, &type, &name);
if (type == ICN#) {
/* Be sure ICN# is owned by this application (or may be System) */
if (HomeResFile(handle) != CurApRefNum && id != 3) return;
/* Get applications name and directory */
fcb.ioNamePtr = name;
fcb.ioRefNum = CurApRefNum;
fcb.ioFCBIndx = 0;/* Look up by ioRefNum */
error = PBGetFCBInfo(&fcb, FALSE);
/* Get applications signature */
pb.fileParam.ioNamePtr = fcb.ioNamePtr;
pb.fileParam.ioVRefNum = fcb.ioFCBVRefNum;
pb.fileParam.ioDirID = fcb.ioFCBParID;
pb.fileParam.ioFVersNum = 0;
pb.fileParam.ioFDirIndex = 0;
error = PBHGetFInfo(&pb, FALSE);
/* Scan our creator list */
if (error == noErr) {
for (i = 0; i < (GetHandleSize(creators)/6); i++) {
if (pb.fileParam.ioFlFndrInfo.fdCreator ==
(*creators)[i].creator) {
if (id == (*creators)[i].rsrcID) sicnNum = i;
break;
}
}
}
if (sicnNum == -1) return;
/* Replace the original ICN# */
BlockMove(*sicn + (sicnNum << 7), *handle, 128L);
}
}
«listing 1»
Gotchas
Theres a very important appendix in Inside Macintosh, which lists the traps that may move memory. Traps not listed here are guaranteed not to move memory -- you can pass them dereferenced handles, call them at interrupt time, or whatever. Unfortunately, I neglected to notice that _DetachResource was not on the list, but that _CopyBits was (or more truthfully, I forgot to check the list). Actually, _CopyBits doesnt usually move memory -- except when the source and destination bitmaps are a different size.
Only one program broke because Id inadvertently made _DetachResource move memory, but even one was too many -- I rely too much on certain traps not moving memory to give anyone else grief for doing the same. So I moved the doubling to startup time.
There was one small problem with calling a QuickDraw trap like _CopyBits at startup time -- QuickDraw isnt fully initialized. So my doubleSICN (see listing 2) routine opens a new GrafPort and does the _CopyBitsing in it.
/* 3 */
/*
Get and double the size of the SICNs
*/
Handle double_SICN() {
register Handle sicn, bigSicn;
register word sicnNum;
Size size, bigSize;
BitMap sicnBits, icnBits;
GrafPtrsavePort;
GrafPort port;
sicn = GetResource(SICN, 0);
/* Get enough space */
size = GetHandleSize(sicn);
bigSize = size * 4;
asm {
move.l bigSize,D0
_NewHandle SYS ; put it in system heap
move.l A0,bigSicn
}
GetPort(&savePort);
OpenPort(&port);/* Give a valid QuickDraw environment */
SetPort(&port);
HLock(sicn);
HLock(bigSicn);
for (sicnNum = (size >> 5) - 1; sicnNum >= 0; sicnNum--) {
/* Set up the bit map */
sicnBits.rowBytes = 2;
SetRect(&sicnBits.bounds, 0, 0, 16, 16);
sicnBits.baseAddr = *sicn + (sicnNum << 5);
icnBits.rowBytes = 4;
SetRect(&icnBits.bounds,0,0,32,32);
icnBits.baseAddr = *bigSicn + (sicnNum << 7);
CopyBits(&sicnBits, &icnBits, &sicnBits.bounds,
&icnBits.bounds, srcCopy, NIL);
}
HUnlock(sicn);
HUnlock(bigSicn);
ClosePort(&port);
SetPort(savePort);
return(bigSicn);
}
«listing 2»
Testing
The problem with writing INITs is that you have to keep rebooting to test them. However, most of this INIT is a patch to _DetachResource, and patches can be tested within an application. TestAesthete is such an application; it installs the patch when it starts, and restores the original trap address when it quits. Its About dialog plots an ICN# before and after _DetachResourceing it. Its a straightforward Mac application (see listing 3), so I wont discuss it.
The only problem with testing patches is that you really dont want them to be around when you leave your test program, even if you had to _ExitToShell with a debugger. Since the trap table still points to the patch code in your heap, youll crash sooner or later. There is however a low memory global, IAZGlobal, which contains the address of a routine to call before initializing a heap zone; _ExitToShell calls it. With a cleanup routine stuffed in the global, the patch never lasts longer than the program.
Is using a low memory global dangerous for future compatibility? Probably; IAZNotify was stricken from Tech Note 64. It really doesnt matter here, since its only in the test bed, not the final INIT.
/* 4 */
/*
Test the DetachResource patch
*/
#define global
#include aesthete.h
#define IAZGlobal((long *)0x33C)
#define appleMenu1
#define fileMenu 2
#define editMenu 3
#define LAST_MENU3
CursHandlewatch;
MenuHandlemenus[LAST_MENU];
word myRef; /* refNum of my resource file */
long oldTrap;
long oldIAZ; /* Save old IAZNotify */
/*
MAIN
*/
void main() {
WindowPtrwhichWindow;
word code;
char c;
Point pt;
EventRecordevent;
oldIAZ = *IAZGlobal;
*IAZGlobal = (long)notify; /* Install our routine */
initialize();
InitCursor(); /* Back to arrow */
while (TRUE) { /* Main event loop */
SystemTask();
if (GetNextEvent(everyEvent,&event))
switch(event.what) {
case mouseDown:
code = FindWindow(event.where, &whichWindow);
switch (code) {
case inMenuBar:
do_command(MenuSelect(event.where));
break;
case inSysWindow: /* Cursor in system window */
SystemClick(&event,whichWindow);
break;
} /* end switch code */
break;
case keyDown:
case autoKey:
c = event.message & 255;
if (event.modifiers & 256)
do_command(MenuKey(c));
break;
} /* end switch */
} /* end while */
}
/*
ABOUT - Show info about this program
*/
void about() {
register WindowPtrwindow;
WindowRecord wbuf;
EventRecordevent;
Rect bounds;
RGBColor colour;
Handle icon;
window = GetNewWindow(128, &wbuf, -1L);
SetPort(window);
colour.red = 39321;
colour.green = 26214;
colour.blue = 0;
RGBForeColor(&colour);
TextFace(0);
TextFont(newYork);
TextSize(12);
show_string(140,85,3);
icon = GetResource(ICN#, 128);
SetRect(&bounds, 10, 10, 42, 42);
PlotIcon(&bounds, icon);
DetachResource(icon);
SetRect(&bounds, 110, 10, 142, 42);
PlotIcon(&bounds, icon);
SetRect(&bounds, 210, 10, 226, 26);
PlotIcon(&bounds, icon);
while (TRUE) {
SystemTask(); /* Keep clock ticking */
if (GetNextEvent(mDownMask | keyDownMask, &event)) break;
}
CloseWindow(window);
}
/*
CLOSE_ALL - Close all desk accessories
*/
void close_all() {
register WindowPeek w, v;
w = (WindowPeek)FrontWindow();
while (w != NIL) {/* Look at each window */
v = w->nextWindow;/* Remember next */
InitCursor(); /* Normal cursor for MockWrite */
CloseDeskAcc(w->windowKind);
SetCursor(*watch);
w = v; /* Look at next one */
}
}
/*
DO_COMMAND - Do something from the menu bar
*/
void do_command(result) unsigned long result; {
register word menu, item, i;
char name[64];
menu = result >> 16;
item = result;
switch (menu) {
case appleMenu:
if (item == 1) {/* About */
about();
break;
}
GetItem(menus[appleMenu - 1], item, name);
OpenDeskAcc(name);
break;
case fileMenu:
switch (item) {
case 4:/* Quit */
SetCursor(*watch);
close_all();
ExitToShell(); /* IAZNotify restores */
/* original _DetachResource */
}
break;
case editMenu:
SystemEdit(item - 1);
break;
}
HiliteMenu(0); /* Make sure menu bar isnt highlighted*/
}
/*
INITIALIZE
*/
void initialize() {
InitGraf(&thePort);
watch = GetCursor(4); /* Get watch from resource */
SetCursor(*watch);
FlushEvents(everyEvent, 0);
InitFonts();
InitWindows();
InitDialogs(0L);/* No disaster function */
TEInit();
MaxApplZone(); /* Pre-grow */
setup_menus();
install_patch();
}
/*
INSTALL_PATCH -- Install patch (in Application Heap for now)
*/
void install_patch() {
register word saveRef, first, numTypes;
Handle sicn, creators;
OSErr error;
asm {
bra @around
dc.l 0 ; for oldTrap
dc.l 0 ; for creators handle
dc.l 0 ; for sicn handle
#define oCreators-8
#define oSicn -4
myPatch:
lea @myPatch,A0
move.l oCreators(A0),-(SP)
move.l oSicn(A0),-(SP)
move.l 12(SP),A0; original parameter to _DetachResource
move.l A0,-(SP)
jsr aesthetize ; aesthetize(rsrc,sicn,creators)
add.l #12,SP
move.l oldTrap,A0
jmp (A0); pass call to original _DetachResource
_Debugger; should never get here!
around:
move.w #0xA992,D0
_GetTrapAddress
move.l A0,oldTrap ; save original trap address
lea @myPatch,A0
move.w #0xA992,D0
_SetTrapAddress ; install our patch
}
sicn = double_SICN();
creators = GetResource(SIG#,0);
DetachResource(creators);
asm {
lea @myPatch,A0
move.l sicn,oSicn(A0) ; stash it
move.l creators,oCreators(A0); stash it
}
}
/*
KILL_PATCH -- Unpatch our mods to _DetachResource
*/
void kill_patch () {
asm {
move.l oldTrap,A0 ; global holds old address
move.w #0xA992,D0
_SetTrapAddress ; everythings back to original state
}
}
/*
NOTIFY - Clean things up if we _ExitToShell from debugger
This routine gets called from our own _ExitToShell too
*/
void notify() {
long trap;
Handle h;
*IAZGlobal = oldIAZ;
kill_patch(); /* Restore original _DetachResource */
}
/*
SETUP_MENUS - Set up menus
*/
void setup_menus() {
register word i;
register Handle handle;
long type;
word code;
char string[256];
InitMenus();
for (i = 0; i < LAST_MENU; ) menus[i] = GetMenu(++i);
AddResMenu(menus[appleMenu - 1], DRVR);
for (i = 0; i < LAST_MENU; i++) {
InsertMenu(menus[i], 0);
}
DrawMenuBar();
}
/*
SHOW_STRING
*/
void show_string(h, v, rsrc) word h, v, rsrc; {
register StringHandle s;
MoveTo(h, v);
s = GetString(rsrc);
HLock(s);
DrawString(*s);
HUnlock(s);
}
«listing 3»
The INIT
Once the patch worked, it was simple to write an INIT that installed it (see listing 4). I follow the convention that the user can disable the INIT by holding the [Shift] key at startup time, and show an icon using Paul Mercers routines (here encapsulated into the code resource SHOW 0). I then blow my SICNs up to the size of ICN#s, detach my list of creators from the resource map (before installing my _DetachResource patch, youll notice), then install my patch into the trap table.
I find the resource Im in by doing GetResource(INIT,0) which works, but means the INIT cant be renumbered. It would have been better to use RecoverHandle(main). Its too late for that now, and I want to point out that not all INITs can be renumbered (if, for example, you wanted to combine several into one file).
When you compile the INIT, be sure that its nonpurgeable, and loaded into the system heap.
/* 5 */
/*
Install the DetachResource patch
*/
void main() {
Handle handle;
Handle sicn, creators;
asm {
btst #0,0x17B ; check KeyMap+7 -- is [Shift] key down?
bne @noInstall ; yes -- dont install patch
}
handle = GetResource(INIT, 0);
DetachResource(handle); /* Detach ourselves */
handle = GetResource(SHOW, 0); /* Get a handle to PROC */
if (handle != 0L) { /* Loaded OK */
HLock(handle); /* Hold down the PROC */
CallPascal(128, -1, *handle);/* ShowICON() */
HUnlock(handle);/* Let it float in the heap again */
}
asm {
bra @around
dc.l 0 ; for oldTrap
dc.l 0 ; for creators handle
dc.l 0 ; for sicn handle
#define oldTrap -12
#define oCreators-8
#define oSicn -4
myPatch:
lea @myPatch,A0
move.l oCreators(A0),-(SP)
move.l oSicn(A0),-(SP)
move.l 12(SP),A0; original parameter to _DetachResource
move.l A0,-(SP)
jsr aesthetize ; aesthetize(rsrc,sicn,creators)
add.l #12,SP
lea @myPatch,A0
move.l oldTrap(A0),A0
jmp (A0); pass call to original _DetachResource
_Debugger; should never get here!
}
/* 6 */
asm {
around:
}
sicn = double_SICN(); /* Get double-size sicns */
creators = GetResource(SIG#, 0);
DetachResource(creators);
/* 7 */
asm {
move.w #0xA992,D0
_GetTrapAddress
lea @myPatch,A1
move.l A0,oldTrap(A1) ; save original trap address
lea @myPatch,A0
move.w #0xA992,D0
_SetTrapAddress ; install our patch
lea @myPatch,A0
move.l sicn,oSicn(A0) ; stash it
move.l creators,oCreators(A0)
noInstall:
}
}
«listing 4»
Wrapping things up
Ive included an edited listing of the important resources (see listing 5). Personally, I create them all with ResEdit. Note that any file, not just applications, can have a unique icon by including the various bundle resources; just make sure the Bundle bits set.
Why the name? If someone whos athletic is an athlete, someone aesthetic must be an aesthete
/* 8 */
/* Aesthete resources */
resource SICN (0, sysheap) {
{ /* array: 29 elements */
}
};
resource ICN# (128, purgeable) {
/* Our Finder icon (also used at startup) */
};
resource FREF (128) {
INIT,
0,
};
resource BNDL (128) {
æsth,
0,
{ /* array TypeArray: 2 elements */
ICN#,
{
0, 128
},
FREF,
{
0, 128
}
}
};
data æsth (0, purgeable) {/* Signature resource */
$2D A9 31 39 38 39 20 44 61 76 69 64 20 44 75 6E
$68 61 6D 0D 43 6F 72 72 65 63 74 73 20 4D 75 6C
$74 69 46 69 6E 64 65 72 20 69 63 6F 6E 73"
};
data sysz (0, purgeable) {/* Reserve system heap */
$00 00 0C 90"
};
resource cicn (128) {
/* The color icon displayed at startup */
};
data TMPL (6001, SIG#, purgeable) {
$05 2A 2A 2A 2A 2A 4C 53 54 42 07 43 72 65 61 74"
$6F 72 54 4E 41 4D 07 49 43 4E 23 20 69 64 44 57"
$52 44 05 2A 2A 2A 2A 2A 4C 53 54 45"
};
data SIG# (0, sysheap) {/* Application signature list */
$4D 41 43 53 00 03 41 43 54 41 00 80 41 43 54 41"
$01 02 57 49 4C 44 00 80 52 53 45 44 00 80 54 57"
$4B 53 00 80 47 45 4F 4C 00 80 6D 61 63 73 00 80"
$52 65 64 78 00 80 53 49 54 21 00 80 70 72 6D 74"
$00 80 54 52 50 5A 00 80 44 46 42 4F 00 80 53 70"
$69 6E 00 80 4D 57 49 49 03 E8 41 4C 50 45 00 80"
$58 43 45 4C 00 80 53 4F 4C 49 00 80 50 45 52 4C
$00 80 51 45 44 31 00 80 44 5A 54 34 00 80 44 4D
$4F 56 00 80 46 45 44 2B 00 80 74 74 78 74 00 80"
$41 52 54 5A 00 80 53 50 4E 54 00 80 43 52 50 52"
$00 80 4D 50 4E 54 00 80 4D 53 57 44 00 80"
};
«listing 5»
Appendix: Extending Aesthete
If youd like to add your own SICNs to Aesthete, youll need to add the small icon to SICN 0. Open it in ResEdit, choose a small icon near the end, and choose New (or Duplicate). You can Paste a picture from the clipboard, if youve drawn it in another program.
Youll also have to add an entry in the SIG# 0 resource. Aesthete as distributed contains a ResEdit TMPL, so you can easily edit this. Find the place you added your icon, select the *****, and choose New. The entries are the applications signature, and the id of its ICN# (usually 128).
Note that its possible for two versions of an application to use two different ICN#s.
If you make a lot of additions, you may want to increase the amount of space Aesthete reserves at boot time by editing sysz 0.
David Dunham
10635 NE 29th #119
Bellevue, WA 98004-2007