September 93 - FLOATING WINDOWS: KEEPING AFLOAT IN THE WINDOW MANAGER
FLOATING WINDOWS: KEEPING AFLOAT IN THE WINDOW
MANAGER
DEAN YU
These days having floating windows in an application is like having an air bag in a car;
you're not cool if you don't have at least one. Because system software doesn't support
floating windows in the Window Manager, myriad floating window implementations
abound, ranging from the straightforward to the twisted. This article presents a
library of routines providing standard, predictable floating window behavior that most
applications can readily use.
Floating windows are windows that stay in front of document windows and provide the
user easy access to an application's tools and controls. Ever since the introduction of
HyperCard, most Macintosh programmers have been in love with floating palettes and
frequently use them. This would be fine if there were an official way to implement
floating windows, but there is no such beast. This article offers a solution.
Currently, the most popular way of implementing floating windows is to patch various
Window Manager routines so that they behave correctly when floating windows are
present. But patching traps has always been problematical. Patches often make
assumptions about how a particular routine behaves or when it will be called. If
system software or a third-party extension suddenly uses the patched routine where it
has never been used before, compatibility problems can arise. Often, patches subtly
alter the behavior of a routine -- for example, by using a register or setting a
condition code. This makes it difficult for Apple to extend (or even fix!) the
Macintosh API and still maintain a high level of compatibility with existing
applications.
You can just as easily implement floating windows by avoiding the use of high-level
Window Manager routines that generate activate events; instead, you can use
lower-level routines almost exclusively. It's much less likely that the code will break
(or cause other code to break) when Apple makes changes to the system software. The
reason for this is simple: it's much less likely for system software engineers to change
the fundamental behavior of a Macintosh Toolbox routine than it is for them to use that
routine in some new and different way. Under this second implementation method, the
application becomes a proper client of the Toolbox, using the routines that are
available rather than trying to reengineer them. The floating windows library
described in this article and provided on this issue's CD follows this philosophy.
Figure 1 Order of Windows on a Screen
STANDARD FLOATING WINDOW BEHAVIOR
Developers implementing floating windows should follow certain rules to ensure the
"consistent user experience" that we're always harping about. Don't worry if there
seem to be a lot of things to keep in mind; the routines in the library do most of the
hard work for you.
ORDER OF ON-SCREEN INTERFACE OBJECTS
As more and more things appear on users' screens, it becomes very important to define
a front-to- back order in which interface objects appear. This alleviates confusion and
prevents the neophyte user from being scared away when things start flying thick and
fast on the screen. Within an application, the order of windows and other on-screen
objects from back to front should be as follows (see Figure 1):
• Document windows and modeless dialogs
• Floating windows
• Modal dialogs and alerts
• System windows
• Menus
• Help balloons
If you thought that floating windows would be as far back as they are, you get a gold
star. The rationale for putting modal dialogs in front of floating windows stems from
the normal use of these windows: floating windows are most frequently used as tool
palettes. The user picks a tool, color, or something similar from the palette and then
performs an operation on the active document. When a modal dialog appears, the
application needs more information from the user before it can proceed. The tools in
the floating window should not be available because they can't be used in the dialog.
Incidentally, system windows are windows that can appear in an application's window
list but aren't directly created by the application. These windows appear in front of all
windows created by the application. Examples of system windows include notification
dialogs, the PPC Browser, and input method windows.
APPEARANCE OF FLOATING WINDOWS
The physical appearance of the HyperCard floating palette has become the de facto
standard look for floating windows. The description of floating windows that follows is
based on this look. There's at least one popular program that uses the standard
document window as a floating window. Don't do this; it only confuses the novice user.
Unlike document windows, floating windows are all peers of each other. That is, there's
no visual cue to the user of any front-to-back order unless the floating windows
actually overlap each other; they all float at the same level. Because of this equality,
the title bars of floating windows are almost always in an active state. The exception to
this rule occurs when a modal window is presented to the user; since this type of
window appears above floating windows on the screen, the background of the title bar
of each visible floating window turns from its dotted pattern to white to indicate an
inactive state (see Figure 2).
A floating window can have a close box, a zoom box, and a title. The use of size boxes in
floating windows is not recommended. The title bar of a floating window should be 11
pixels high or 2 pixels higher than the minimum height of the primary script's
application font, whichever is greater. The title of a floating window should be in the
application font, bold, and its size should be the greater of 9 points and the smallest
recommended point size for that script system. Floating windows should have a
1-pixel drop shadow that starts 2 pixels from the left and top of the window.
Figure 2 Active and Inactive States of a Floating Window
FLOATING WINDOWS AND CONTEXT SWITCHING
Because floating windows are almost always in an active state, it would be very
confusing to the user if floating windows were still visible when an application is
placed in the background. (Imagine an active window lurking behind an inactive
document window.) For this reason, when an application receives a suspend event it
should hide any visible floating windows. Conversely, when the application receives a
subsequent resume event, the floating windows that were hidden on the suspend event
should be revealed.
IMPLEMENTING FLOATING WINDOWS IN YOUR APPLICATION
Now that we've taken care of the formalities, we can get to the heart of the matter. This
section explains the methodology used in creating the floating windows library
routines included on this issue's CD. (You can use these routines, or you can write
your own using the same methodology.) First, we talk about handling activate events,
which is the trickiest aspect of implementing floating windows. Then, we describe the
API in the floating windows library and how you can use it in your applications.
DEALING WITH ACTIVATE EVENTS
The most difficult part of implementing floating windows is dealing with activate
events. You need to work around how the Window Manager generates these events and
how the Toolbox Event Manager reports them to an application. The Window Manager
was written under the assumption that there's only one active window at any time;
obviously, this is not true in an application that has floating windows. A corollary of
this assumption is that the Window Manager generates only one deactivate event for
every activate event. This model breaks down when a modal dialog appears in an
application with floating windows: the modal dialog receives the activate event, but a
deactivate event is necessary for all visible floating windows and the frontmost
document window. If things were left up to the Window Manager, only the frontmost
floating window would receive the required deactivate event.
To avoid this problem, you shouldn't use the Window Manager routines SelectWindow,
ShowWindow, and HideWindow since they implicitly generate activate and deactivate
events. In addition, you shouldn't use SendBehind to move the front window further
back in the pile of windows on the screen or to make a window frontmost, because that
routine also generates activate events.
Instead, use lower-level routines like BringToFront, ShowHide, and HiliteWindow to
simulate the higher-level calls. Additionally, instead of dispatching activate events in
your application's main event loop, you should activate or deactivate a window as its
position in the window list changes. Here's how a replacement to SelectWindow might
look (see "This Is Not Your Father's Window Manager" for more information on this
routine):
pascal void SelectReferencedWindow(WindowRef windowToSelect)
{
WindowRef currentFrontWindow;
WindowRef lastFloatingWindow;
ActivateHandlerUPP activateProc;
Boolean isFloatingWindow;
if (GetWindowKind(windowToSelect) == kApplicationFloaterKind) {
isFloatingWindow = true;
currentFrontWindow = (WindowRef) FrontWindow();
}
else {
isFloatingWindow = false;
currentFrontWindow = FrontNonFloatingWindow();
lastFloatingWindow = LastFloatingWindow();
}
// Be fast (and lazy) and do nothing if you don't have to.
if (currentFrontWindow != windowToSelect) {
// Selecting floating windows is easy, since they're always
// active.
if (isFloatingWindow)
BringToFront((WindowPtr) windowToSelect);
else {
// If there are no floating windows, call SelectWindow
// as in the good ol' days.
if (lastFloatingWindow == nil)
SelectWindow((WindowPtr) windowToSelect);
else {
// Get the activate event handler for the window
// currently in front.
activateProc =
GetActivateHandlerProc(currentFrontWindow);
// Unhighlight it.
HiliteWindow((WindowPtr) currentFrontWindow, false);
// Call the activate handler for this window to
// deactivate the window.
if (activateProc != nil)
CallActivateHandlerProc(activateProc,
uppActivateHandlerProcInfo,
currentFrontWindow,
kDeactivateWindow);
// Get the activate event handler for the window
// that's being brought to the front.
activateProc = GetActivateHandlerProc(windowToSelect);
// Bring it behind the last floating window and
// highlight it. Note that Inside Macintosh Volume I
// states that you need to call PaintOne and CalcVis
// on a window if you're using SendBehind to bring it
// closer to the front. In System 7, this is no
// longer necessary.
SendBehind((WindowPtr) windowToSelect,
(WindowPtr) lastFloatingWindow);
HiliteWindow((WindowPtr) windowToSelect, true);
// Now call the window's activate event handler.
if (activateProc != nil)
CallActivateHandlerProc(activateProc,
uppActivateHandlerProcInfo, windowToSelect,
kActivateWindow);
}
}
}
}
Activate events and the frontmost document window. Other cases that the
Window Manager doesn't handle well occur when the frontmost document window is
closed or when a new document window is created in front of other document windows.
If floating windows are present, these document windows don't get the needed activate
and deactivate events, since the application is essentially removing or creating
windows in the middle of the window list. Your application needs to send the right
activate events to the right windows. The floating windows library routines
ShowReferencedWindow and HideReferencedWindow generate the appropriate activate
and deactivate events for you.
Activate events and modal windows. When a modal window is to appear, you
should send deactivate events to all visible floating windows and to the active document
window. When the user dismisses the modal window, send activate events to those
windows. Instead of overloading SelectReferencedWindow with yet another case, it's
easier to surround calls to Alert or ModalDialog with calls to deactivate and activate
the floating windows and the first document window.
Here's what the code would look like:
short PresentAlert(short alertID, ModalFilterProcPtr filterProc)
{
short alertResult;
DeactivateFloatersAndFirstDocumentWindow();
alertResult = Alert(alertID, filterProc);
ActivateFloatersAndFirstDocumentWindow();
return alertResult;
}
THIS IS NOT YOUR FATHER'S WINDOW MANAGER
You may have noticed that the SelectReferencedWindow routine doesn't strictly define
how to do certain things. There are two reasons for this. The first is the advent of
PowerPC architecture. When you write code that has the potential of running on
several different runtime architectures, it should be generic, especially if you don't
know what's lurking on the other side of a procedure pointer. The 68000 and PowerPC
architectures handle procedure pointers differently: on a 680x0 machine, a ProcPtr
points to the entry point of a procedure, whereas on a PowerPC, a ProcPtr points to a
routine descriptor. It would be nice if source code that calls procedure pointers didn't
have to worry about the proper calling convention for a particular platform and the
proper magic would happen at the flip of a compile switch. The solution that we use in
system software is the CallProcPtr macros defined in our interface files, which
expand to different things depending on the platform we're compiling for. For the
ActivateHandlerUPP (for Universal Procedure Pointer) type used in
SelectReferencedWindow, the definitions shown below are needed.
The second reason for generality in the code is the future. We would like to move the
Macintosh operating system into the 1990s to get preemptive multitasking and
separate address spaces. This means a move toward opaque data structures: accessor
functions will be provided, so you won't be able to access fields of a data structure
directly. In the future, data structures like WindowRecords may no longer be created
in your application's address space, so you'll get a reference to a window instead of an
absolute address. The floating window API follows this philosophy; all calls take a
WindowRef type instead of a WindowPtr, and all fields of a window's data structure are
accessed with an accessor function. This is all for the best. Really.
typedef pascal void (*ActivateHandlerProcPtr)(WindowRef theWindow,