One App Patches
Volume Number: 8
Issue Number: 6
Column Tag: C Workshop
Related Info: Calling a Code Resource Window Manager
Process Manager
One-Application Patches
How to write an application specific extension.
James W. Walker, University of South Carolina
About the author
James W. Walker earned a Ph.D in mathematics at M.I.T. He now teaches
mathematics at the University of South Carolina.
Not Quite an INIT
To make a change in the behavior of all applications running on your Mac, you
can use an INIT (now known as a system extension) to patch some traps. The trouble
with this approach is that it has to be compatible with all of the applications, and
probably imposes some overhead even in applications where it isn’t doing anything. On
the other hand, you could disassemble one application and make a direct patch. Not only
is that likely to be extremely difficult, you will probably have to do it over again when
the next version comes out. There is a middle ground: Code resources that can be added
to an application and patch traps only in that application.
Under MultiFinder or System 7, trap patches that are installed after startup
time apply to only one application, because each application has its own copy of the trap
dispatch table. That’s the easy part. The tricky part is, how do you get your code called
in order to install the patches? What you can do is use your own version of one of the
standard definition functions, such as a WDEF, MDEF, MBDF, or CDEF. In my example,
I will use a WDEF, since that makes it easy to modify the appearance of windows in an
application. That is the approach used by the CMaster and PopUpFuncs products.
Adding Word Wrapping and Dollar Pairs
All word processors can wrap words as you type them, but not all text editors
can do so. For instance, BBEdit 2.1.3 can wrap text after you type it, but not as you
type it, and the THINK C 5.0 editor cannot wrap words at all. (You probably wouldn’t
want to use word wrapping while writing program code, but you might want it for long
comments.) My example will patch an editor to provide a simple form of word
wrapping. It will also add a little icon to the title bar of each document window, which
you can click to turn wrapping on or off. This patch will work in THINK C, BBEdit, or
ASLEdit+.
In order to wrap typing, we would ideally want to detect when the insertion point
has passed the right edge of the window or some other preset margin, and then change a
previous space character into a carriage return. However, that would be difficult to do
without knowing the application’s internal data structures. Therefore, I am going to do
a cruder form of word wrapping: Detect when the insertion point is within a certain
distance of the right edge of the window, and then change the next space to a carriage
return. This method can fail if you happen to type a really long word at the end of a
line, but it usually works.
As another example of a feature that can be added with a one-application patch, I
will make each typed dollar sign generate another dollar sign and a left arrow character.
Sound like a crazy feature? Not if you’re typing mathematics in TEX format, which
uses pairs of dollar signs to delimit mathematical formulas.
A Modular Design
The project will use four types of code resources, so that individual functions
can be added or deleted without recompilation. At the top of the hierarchy (illustrated
below), there is one WDEF resource. At the next level, there are OAPn resources that
are called by the WDEF after each wNew message, and OAPd resources that are called by
the WDEF after each wDraw message. One of the OAPn resources installs an event
patch, and the other one watches the insertion point. The OAPd code draws a small icon
in the window’s title bar. Finally, at the third level of the hierarchy, there are OAPe
resources, which filter events. There is also a small data resource of type OAP1 which
is used for communication between some of the code resources.
Each of these code resources is built as a separate THINK C project. All require
MacHeaders, and some need the MacTraps library.
Figure One: Calling Hierarchy
The WDEF
In order for our WDEF to be used for standard windows, we must use the
resource ID 0, and override the standard WDEF in the System. However, it calls the
standard WDEF to do most of the work. I use RGetResource just in case the standard
WDEF 0 is in ROM and not in the System. Incidentally, you should be aware that adding a
WDEF resource might trigger virus detection code in some applications. [See Nick
Pissaro article in Vol. 8, No. 2 (Virus issue) for one example. - TechEd.]
One tricky aspect of using a WDEF to patch an application is that if you use
ResEdit to edit a WIND resource in that application, the custom WDEF may be called. If
the WDEF patches some traps, and then ResEdit closes the file, then the traps remain
patched but the patch code goes away. So the next time one of those traps is executed,
it’s bomb city. I found out about this the hard way, of course.
To avoid this ResEdit problem, I use the routine No_ResEdit_Danger (see the
listing of patcher WDEF.c), which checks whether the file that contains the WDEF
resource is the same as the resource fork of the current application. If not, the WDEF
does nothing other than call the real WDEF to handle the window. (Desk accessories are
a special case. Although they act like applications in many ways under System 7,
CurApRefnum is the file reference number of the System, not the DA file.)
The Wrapping Icon
The ‘OAPd’ resource, whose source code is shown in the listing of wrap icon.c, is
called by my WDEF after each wDraw message for a document-style window. I have
hard-coded the two possible 8 by 8 icons, though of course one could use resources
instead.
Where’s the Insertion Point?
To perform word wrapping, we need to know where characters are appearing in
the window. One natural approach would be to look at the pen location of the window at
the time that a keyboard event is received. This works in THINK C and BBEdit, but not
in ASLEdit+. You might also think of patching _DrawChar to watch as characters are
drawn, but in fact these editors do not call DrawChar. The only approach I thought of
that works in all three cases is to patch _InverRect and watch where the insertion
point is drawn. When InvertRect is called with a rectangle of width 1, it is probably
flashing the insertion point.
In the listing, you will see that the patch is installed using routines named
GetToolTrapAddress and SetToolTrapAddress. These are not listed in Inside Macintosh,
but are defined in the standard header file OSUtils.h. They simply provide a more
efficient interface to the same trap routines used by NGetTrapAddress and
NSetTrapAddress.
Assembly Glue
Some folks will insist that when you patch traps, you should save and restore
every blessed register. Others will point out that Inside Mac says that stack-based
toolbox routines need not preserve registers A0, A1, D0, D1, or D2, so a patch on such
traps shouldn’t need to preserve those registers either. In the trap patches in the
InvertRect.c and events.c listings, I have taken the very conservative route of
preserving all registers. If you choose not to preserve all registers, then the only
register you really have to worry about is A4, which is used by THINK C to access
global variables. You could begin the patch with
/* 1 */
asm {
move.L A4, -(SP)
LEA main, A4
}
and end the patch with something like
/* 2 */
asm {
move.L Old_SystemEvent, A0
move.L (SP)+, A4
UNLK A6
JMP (A0)
}
However, if you do it, remember that if the prior trap address is a global
variable that is referenced using register A4, then you had better use that value before
you restore the original value of A4.
Watching Events
There are a number of ways you can monitor events. You can tail-patch
GetNextEvent, tail-patch GetOSEvent, patch the low-memory global JGNEFilter,
head-patch PostEvent, or head-patch SystemEvent, and there are probably other ways.
However, these methods do not all behave the same. Patching GetNextEvent will miss
events destined for desk accessories, even DAs that have been made into
pseudo- applications under System 7. On the other hand, JGNEFilter is truly global,
i.e., it will see events belonging to other applications. I have chosen to patch
SystemEvent. For some purposes, the fact that SystemEvent doesn’t receive null events
might be a disadvantage, but not for my present purpose.
The listing events.c shows the patch to SystemEvent, which passes each event to
any OAPe resources that may be present.
Word Wrapping Events
The event filter listed in wrap events.c monitors keyboard events to perform
word wrapping, and monitors mouse events to detect clicks in the word wrapping icon.
If wrapping is on, and the event is a space character, and the insertion point is close to
the margin, then the event filter changes the event to a return character. If the event
is a mouse click in the wrapping icon, then the event filter toggles the wrapping state,
changes the event to a null event (so that the host application won’t think you’re trying
to drag the window), and causes the wrapping icon to be redrawn. Note in particular
that when I call PaintOne to invalidate the wrapping icon, I save and restore the
GrafPort. This is necessary because PaintOne changes to the Window Manager port, and
does not restore the port afterward.
Paired Dollar Signs
The final event filter, listed in dollars.c, looks for keyDown events representing
dollar sign characters, and responds to a dollar sign by posting another dollar sign
event and a left arrow event. I have to be careful about this in order to avoid an infinite
loop. A normal keyboard event has both a character code and a key code in the message
field of the event record, but when I post the second dollar sign, I post only a character
code without a key code. Then when the second dollar sign arrives at the event filter,