Tool Window
Volume Number: 4
Issue Number: 12
Column Tag: C Workshop
Tool Window Wanager 
By Thomas Fruin, The Netherlands
The Tool Window Manager
Thomas Fruin is a graduate student of Computer Science at Leiden Universit, who
also does study-related work for the two universities in Amsterdam, the University of
Amsterdam and the Free University. He is the president and co-founder of VAMP
(Vereniging Apple Macintosh Programmeurs), the main Dutch association for
Macintosh programmers. Otherwise, he has done various odd Mac-related jobs to earn
money, usually writing software (educational, communications, etc.).
The Macintosh user interface has been enhanced with some interesting new
features lately: tear-off menus, pop-up menus, hierarchical menus, window layers,
color, and even a new name: human interface. While most of these additions are in the
new ROMs and the latest System file, not everything has been dealt with by Apple.
Most notably is the lack of code to support tear-off menus, the most recent novelty and
pioneered by HyperCard. They are something you are going to have to code all by
yourself (well, with a bit of help from others). Darryl Lovato addressed the problem
of tearing off a menu first, followed by an in depth article by Don Melton and Mike
Ritter. In my contribution I will concentrate on keeping windows (torn-off menus or
otherwise) floating above the others on your desktop, by presenting an alternative
window manager - called the Tool Window Manager - that does all the work.
While tear-off menus are new to the Mac, floating windows are not. For several
years now there have been applications putting their tools, patterns and other wares in
these little windows, like FullPaint and PageMaker to name just two. Tool windows are
a tricky business though, and it takes some careful planning to get everything right.
Even commercial programs make mistakes at this (see the sidebar on the bugs in
FullPaint and MacPaint).
Tool Windows Have a Special Appearance
First of all, since tool windows always lie on top, there is no distinction between
an active or inactive tool window. This means that the tool should either have no title
bar (like in FullPaint), or a title bar that never changes its appearance (like in
HyperCard or Claris MacPaint). The latter case requires a special window definition
function. This is not the subject of this article, however, since window definition
functions have already been dealt with by MacTutor. Besides, you can cheat by
experimenting with the WDEF for tool windows in the new MacPaint. Just copy WDEF
34 from your copy of MacPaint with ResEdit and paste it in your own application. Of
course you cannot distribute it ... (but you knew that, didn’t you?)
Don Melton and Mike Ritter’s comments on the user interface guidelines for tool
windows, and e specially their additions to the guidelines, are a good idea. Since the
article came out, I have modified my manager to incorporate these additions: when a
non-application window is brought to the front (usually a desk accessory), my
manager will unhighlight all the tool windows. When that window disappears, all the
tools are highlighted again. I also added this behavior when a modal dialog window is
brought to the front, because it seems to make sense. Finally, the unhighlighting of the
tool windows also takes place when another application is activated under MultiFinder.
It is up to the application that uses the Tool Window Manager to hide the tool windows
when the application is juggled out, since it may not always want to hide the tools (one
of them may be some kind of status window, for example).
Tool Windows or GhostWindows?
Tool windows are also sometimes called ghost windows. This is probably due to
the obscure GhostWindow low memory global that is mentioned in Inside Macintosh
volume I, page 287. You had better forget about it. I’m not only doing this as a public
service (since Apple is discouraging people to use low memory globals), but also
because it really doesn’t help in our case. As in almost every part of Inside Macintosh,
the information is there, but it takes some careful reading to understand what the
paragraph is about. Besides, GhostWindow will only let you have one tool window at a
time, and we want to create as many tool windows as will fit in memory (it’s ok with
me if you only want two or three). So it seems the only way out is a front end for all
the Window Manager routines that could cause a tool window to become obscured. This
is what I have written in the Tool Window Manager, or TWindow Manager for short.
The Mad T Party
The TWindow Manager is a set of 10 functions that replace calls to functions in
the Window and Event Managers in your source file. These new functions do some extra
processing before they call the Window and Event Manager themselves. The names of
these TWindow Manager routines are the same as their corresponding Window and
Event Manager functions, except for a prefixed “T”, as in TSelectWindow and
TGetNextEvent. Their parameters are almost the same as the original functions, with a
few slight modifications. See the explanations below for details. There are six
categories of window manipulations that need to be replaced by TWindow Manager
functions to work correctly with tool windows: (1) Adding windows to the desktop: use
TNewWindow, TGetNewWindow and TShowWindow. (2) Removing windows: use
TCloseWindow, TDisposeWindow and THideWindow. (3) Bringing lower windows to the
top with TSelectWindow. (4) Dragging with TDragWindow. (5) Utility functions you
will need every now and then: TFrontWindow and the new TInitWindows and
TGetWKind. (6) Event handling: TGetNextEvent. This last function is included,
because activate and deactivate events are handled differently when there are tool
windows about. Note that I did not mention resizing windows (yes, I’m mentioning it
now, thank you). Amazingly, GrowWindow is smart enough to draw its grow outline
below any windows that are lying on top of it. It’s always nice to have less work to do.
There are three source files that go with this article. TWindows.c is the
TWindow Manager itself, written in MPW C, and can be compiled into a separate
module for later linking with your own program. TWindows.h is a file for inclusion by
your program. It holds common data types and the TWindow function types. Your
program will need this file if it wants to call TWindow Manager functions and use
constants such as toolKind and anyKind. Finally, TWindowTester.c (also in MPW C) is
an example program that uses the TWindow Manager. It is a familiar ‘vanilla’
Macintosh program that lets you create normal and tool windows to experiment with,
as may as you like (see figure 1). You can select and drag windows, and open desk
accessories as well. (Note that I got the tool window definition function from MacPaint
2.0, as I described above. In the source I am distributing, the windows look more like
FullPaint’s tools, because MacPaint’s defproc is not included.) The menus called
Windows and Tools let you make any document window or tool invisible and visible
again, even if it isn’t in front. I added these menus myself to test the robustness of the
TWindow Manager. Note that you can’t do anything useful with TWindowTester
(although you might derive great pleasure from opening, say, 100 windows). For
example, the scroll bars in the document windows don’t work. I just put them there so
you can see that windows get activated and deactivated properly.
Background Information, Utility Functions
The TWindow Manager keeps tool windows and document windows apart by storing
a special value in the WindowRecord’s windowKind field. This value is defined as the
toolKind constant (equal to 30000) in the source file for TWindows. If you like, you
can modify it to a value more suitable to your application, but there should be little
conflicts as is. If any of the TWindow Manager functions wants to know what kind of
window it is dealing with, it calls the utility function TGetWKind. This function is
precisely for determining the windowKind value of a window. Why write a function for
such a simple task? Well, TGetWKind doesn’t just return the window’s windowKind,
but does some extra processing as well. If the window is a desk accessory window
(with a negative windowKind), TGetWKind will always return the constant systemKind
(equal to -1). If the window is a dialog window, with windowKind equal to the toolbox
constant dialogKind, TGetWKind only returns dialogKind if the window is a modal dialog
window. If the dialog is modeless, TGetWKind returns the toolbox constant userKind
(the toolbox constant for normal document windows). This is because modeless dialog
windows behave almost exactly the same as document windows, i.e. they lie behind tool
windows. TGetWKind figures out what kind of dialog it is by examining the window’s
variation code and definition procedure. Window definition procedures have an
(optional) header embedded in their code that holds information like the WDEF’s
resource ID (see Technical Note 110 for more information about WDEFs - and make
sure you have the “real” 110). The dialog is considered modeless if its window is a
document window: this is when the WDEF’s resource ID is zero and the variation code
is equal to documentProc, noGrowDocProc or zoomDocProc. To play it safe, I let
TGetWKind also check if the resource type in the header is really ‘WDEF’. If the WDEF
doesn’t have a header (like the MacPaint tool window for example), it’s extremely
unlikely that there will be a ‘WDEF’ string embedded at that particular position in the
code.
The Window Manager in the Macintosh ROM has (as far as I can determine) only
one global variable dealing with window positions, called WindowList - at location
$9D6. It holds the very first window in the window list. By comparison, my TWindow
Manager has three position globals of its own: frontToolWindow, backToolWindow and
frontDocWindow. FrontToolWindow points to the frontmost visible tool window,
backToolWindow to the backmost visible tool window, and frontDocWindow to the
frontmost visible document window. Two other global variables I use mimic the
CurActivate ($A64) and CurDeactive ($A68) low memory globals in the Macintosh:
toBeActivated and toBeDeactivated. They hold the real windows that need activating and
deactivating, and my TGetNextEvent takes them into account when it returns activate
events. They are set by various TWindow Manager routines whenever activate events
need to be posted. Finally I have two utility global variables, firstRgn and secondRgn.
All these seven globals need to be initialized by the TInitWindows function, before you
start using any functions in the TWindow Manager.
Figure 1: The TWindowTester Application
TFrontWindow is my replacement for FrontWindow. You can specify a window
type in the windowKind parameter (note that FrontWindow has no parameters at all).
This window type is one of the constants toolKind, userKind or anyKind. Usually you
will want to call TFrontWindow with windowKind equal to userKind; it will then
return the frontmost visible document window (the active window), ignoring any tool
windows that may be lying above it (unless there is a desk accessory or modal dialog on
top - it then returns the DA’s or dialog’s window). Calling TFrontWindow with
windowKind equal to toolKind returns the frontmost visible tool window (again taking
DAs and dialogs into account). When you call TFrontWindow with anyKind as the
parameter, it behaves exactly like the original FrontWindow function - it simply
returns the frontmost visible window.
Selecting Windows
I’m saving the subjects of adding and removing windows for later, because they
are the trickiest ones to handle. TSelectWindow, by comparison, is much easier.
Here, the main problem is to bring a document window to the “front” while keeping it
behind any tool windows that may be visible. Apple, in its infinite wisdom, already has
a function that almost does this: it’s called SendBehind and it’s listed on page I-286 in
Inside Macintosh. You use it to move a window closer to the front (ah, so that’s why
it’s called SendBehind), and specify the window behind which your window should be
moved. In our case this will usually be the backmost tool window. Note what Inside
Macintosh has to say about this: “If you’re moving theWindow closer to the front (that
is, if it’s initially even further behind behindWindow), you must make the following
calls after calling SendBehind:
wPeek = POINTER( theWindow );
PaintOne( wPeek, wPeek^.strucRgn );
CalcVis( wPeek );
This means that you have to do some low-level stuff, because SendBehind isn’t
really meant to bring windows forward. PaintOne (5 pages further) is a routine that
whitewashes the newly exposed part of the window. CalcVis (on the next page in Inside
Macintosh) recalculates the window’s visRgn. Unfortunately, it seems Apple goofed
here. CalcVis only recalculates the visRgn of one window, while we need to recalculate
the visRgns of all the windows following our window. Apple makes up for this, though,
by supplying us with the routine CalcVisBehind (on same page as CalcVis). This
function does not only recalculate the visRgn of our window, but also of all the windows
following it. If you’re curious, try replacing CalcVisBehind by CalcVis and watch what
happens on the screen (don’t do this if you dislike making a mess of things).
Another thing: the above call to PaintOne paints the whole window white, and not
only the newly exposed part. Therefore, I’ve created the utility function
BringForward, that calls SendBehind, PaintOne and CalcVisBehind, and only repaints
those newly exposed parts. BringForward is called by TSelectWindow and many other
routines in the TWindow Manager.
If you look at the source code for TSelectWindow, you’ll see it does a lot more
than just calling BringForward. This is because we have to take desk accessories into
account. An open desk accessory will always lie above any tool windows when it is
active. If the user selects one of the document windows, it naturally comes to the
front, but not until all of the tool windows have also been brought to the front
(remember that they were lying behind the desk accessory). I’ve written another
utility function for this, obviously called BringToolsForward. This function makes
repeated calls to BringForward for each visible tool window. These calls it makes to
BringForward have a special flag to tell it not to call CalcVisBehind each time. This is
because CalcVisBehind is a relatively time-consuming function. Once all the tool
windows have been brought forward, BringToolsForward makes one final call to
CalcVisBehind, recalculating all those changed visRgns at once. This speeds things up
by about 30%, which is nice when you have a lot of tool windows (at one time during
testing, I created about 50). The speed bottleneck, however, seems to be SendBehind. I
wish Apple would publish interfaces to the Layer Manager in MultiFinder, because it
has much more efficient ways of quickly moving windows to the front. Those aren’t
standard Window Manager functions working there ...
Two other useful utility functions internal to the TWindow Manager are
NextVisWindow and PrevVisWindow. They operate fairly straightforward, and do
exactly what their names imply: NextVisWindow returns the next visible window, and
PrevVisWindow returns the previous visible window in the window list, with respect
to the window that gets passed to it. NextVisWindow and PrevVisWindow are used in
various places in the TWindow Manager, including here in TSelectWindow.
Dragging Windows
TDragWindow replaces the standard toolbox function DragWindow. It lets you
drag a document window, while ensuring that it doesn’t obscure any tool windows that
may be lying in front of it. Again, I am grateful to Apple for doing the dirty work: they
wrote this useful low level function called DragGrayRgn (Inside Macintosh page
I-294) that pulls a gray outline of a region around. DragWindow actually calls
DragGrayRgn, but in the wrong way (for our purposes).
The first thing TDragWindow does is check if the user is holding down the
Command key. It gets this information from the EventRecord that you pass it. (Note
that TDragWindow wants you to pass an EventRecord as its second parameter, while
DragWindow wants the point where the mouse was clicked.) If the Command key is not
being held down, TDragWindow calls TSelectWindow to move the window to the front
(while staying below any tool windows, remember?). Once that is done, we can call
DragGrayRgn to move an outline of the window. This outline is the window’s strucRgn,
so we pass copy of the strucRgn to DragGrayRgn. DragGrayRgn needs some more
parameters, like the point the mouse was originally clicked and the bounding
rectangles for the drag, but they are pretty obvious from the documentation in Inside
Macintosh.
We have one last problem: how do we get DragGrayRgn to draw its outline below
the tool windows? Aha, well, there’s yet another low-level Window Manager function
for that (I hope I’m not boring you). It’s called ClipAbove, and Inside Macintosh says
that “ClipAbove sets the clipRgn of the Window Manager port to be the desktop
intersected with the current clipRgn, minus the structure regions of all the windows
in front of the given window.” Great. So all we have to do is set the current port to the