Try Pop-Up
Volume Number: 1
Issue Number: 13
Column Tag: C Workshop
Try Pop-Up Menus! 
By Mike Schuster, Consulair Corp.
What's a Pop-Up Menu?
The Macintoshâ„¢ user interface provides a repertoire of graphic objects that
perform functions when pressed with the mouse. Pressing a scroll bar's arrow moves
the document under the window. Selecting a tool from a palette determines the type of
object to be drawn. Pressing on a menu title presents a list of choosable actions and
attributes. A feature missing from this repertoire is the ability to associate a menu
with an arbitrary graphic object on the screen. A pop-up menu provides just this
capability.
Here's how it works: When you press on a object with an associated pop-up menu
(the little menu icon above), the menu instantly "pops-up" right on top of the object
and under the pointer. While holding down the mouse button, you move the pointer
down the menu. As pointer moves to each item, the item is highlighted. The item that's
highlighted when you release the mouse button is choosen. As soon as the button is
released, the command blinks briefly, the menu disappears, and the command is
executed, just like a pull-down menu.
A pop-up menu shares many of the advantages of a pull-down menu: It's
invisible until you want to see it, yet at the same time it's easy to see and choose items
from. Until you choose an item, nothing happens, so you can look at a pop-up without
making a commitment to do anything.
The advantage of a pop-up is that it directly associates actions and attributes with
an object on the screen. Its biggest disadvantage is that there may be no indication that
a given object has an associated menu, until you happen to press on it. This
disadvantage could lead to less transparent and less consistent applications, e specially
if the application's standard menus were replaced with pop-ups. Don't do that! I find
them most useful in dialog boxes and desk accessories.
One final issue. When the user presses on an object with a pop-up menu, it
might be desirable to highlight the object and have the menu pop-up just below the
object, in a manner similar to pull-down menus. I'd call such a scheme a "pop-down
menu, and leave its implementation as an exercise.
Designing a Pop-Up
To make pop-up menus easy to use, I wanted to minimize the number of additional
routines an application or desk accessory must call. Also, I wanted to apply all of the
existing resource editing and compiling tools to construction and modification of the
resource file description of a pop-up. Finally, to minimize my programming effort, I
wanted to use as much of the Menu Manager's built in machinery as possible. With
these goals in mind, I decided to use the standard menu record and the standard menu
definition procedure without modification.
My efforts resulted in the single routine PopUpSelect. Its interface is almost
identical to the Menu Manager routine MenuSelect:
typedef short int16;
typedef long int32;
int32 PopUpSelect(theMenu, hitPt)
MenuHandle theMenu;
Point * hitPt;
Here's how you use it: When your application receives a mouse-down event in an
object that has an associated pop-up, your application should call the PopUpSelect,
supplying it with a handle to the pop-up menu and the point where the mouse button
was pressed. PopUpSelect will pop-up the menu, track the mouse and highlight menu
items until the user releases the mouse button. PopUpSelect returns a 32 bit integer
in a manner identical to MenuSelect. The high-order 16 bits contains the menu ID of
the pop-up menu, the low-order 16 bits contains the menu item number of the item
that was choosen. If no item was choosen, the value returned is 0. (I use int16 and
int32 to avoid the ambiguity inherent in the sizes of C's short, int, and long types.
Also, I pass points by address rather than by value, in deference to Inside Mac.)
Before PopUpSelect can be called, the pop-up menu itself must be set up. Since a
pop-up has the same structure as a pull-down menu, you do this by reading the menu
from a resource file using GetMenu, or allocate it with NewMenu and filling it with
items using AppendMenu, AddResMenu or InsertResMenu. The only difference in usage
between pull-down and pop-up menus is that pull-downs are added to the menu bar
using InsertMenu, and pop-ups are passed to PopUpSelect whenever the appropriate
mouse-down events occur.
One thing to note: Don't bother defining a command key equivalent to an item in a
pop-up. Since the pop-up is not in the menu bar, the Menu Manager routine MenuKey
won't be able to find it. You could add all of your pop-ups to the menu bar just before
the MenuKey call, and remove them just after, but don't do this! There's no need for
these items to have command equivalents.
Making It Work
PopUpSelect is built up from the following sequence of operations:
- determine where to draw the pop-up
- save the part of the screen under the pop-up
- draw the pop-up
- track the mouse until the mouse button is released
- blink the selected item, if any
- erase the pop-up and restore the screen
- return the appropriate result
The first task, that of determining where to draw the pop-up, is based on the size
of the menu and the location where the mouse was pressed. The size of the menu is
defined by the fields menuWidth and menuHeight in the menu record (whose definition
is shown below), which specify the horizontal and vertical dimensions of the menu, in
pixels. The Menu Manager determines these values when the menu is allocated and
filled with items.
typedef struct
{
int16 menuID;
int16 menuWidth;
int16 menuHeight;
Handle menuProc;
int32 enableFlags;
Str255 menuData;
} MenuInfo, *MenuPtr, **MenuHandle;
The standard menu definition procedure draws the menu's items inside a
rectangle of these dimensions. The black border and shadow of the standard pull-down
menu appear immediately outside that rectangle.
I decided that the rectangle should be positioned so that the location where the
mouse was pressed is just inside of the top-right corner of the menu. The pop-up
hangs generally downward and to the left of the pointer. The only constraint on this
positioning is that the menu must be completely visible in the desktop portion of the
screen. Thus, the rectangle must be shifted in the appropriate direction if the mouse
location is near the menu bar or an edge of the screen.
The technique I used to determine the coordinates of the menu rectangle can best
be explained with the aid of a diagram. The point labeled S is the top-left corner of the
screen. The point labeled M is the location where the mouse was pressed (defined by
the hitPt argument). The rounded corner rectangle represents the screen. The thick
bordered rectangle containing M represents the boundary of the pop-up.
Notice that top-left corner of the menu rectangle is displaced POINTERH pixels
horizontally and POINTERV pixels vertically from M. These values incorporate my
decision to have the menu appear generally to the left and downward from the pointer.
They are constants in the implementation, and hence are easy to change.
The top of the menu rectangle is displaced 20 pixels downward from the local
origin, labeled 0,0 in the diagram. By defining a local origin in this manner, the
standard menu definition procedure is tricked into thinking that it is drawing the menu
just below the menu bar. That is, the top coordinate of the menu rectangle in the local
system is 20, the height of the standard menu bar. Given these values, you should be
able to find that the horizontal coordinate of S is -(hitPt.h - POINTERH) and its
vertical coordinate is -(hitPt.v - POINTERV - 20).
The final constraint on the local location of S is that the menu must be completely
visible in the desktop portion of the screen. This constrains the horizontal coordinate
of S to be less than or equal to 0 (otherwise the left side of the menu would be
off-screen) and greater than or equal to the width of the menu minus the width of the
screen (otherwise the right side would be off-screen). A similar situation holds
vertically. All of these computations (plus an allowance for the menu's frame and
shadow) are incorporated in the arguments of a Quickdraw SetOrigin call, which sets
up the desired local origin.
One final item to note: SetOrigin translates the coordinates of the current port's
portBits.bounds, portRect, and visRgn to the new coordinate system, but not its
clipRgn. Hence, a call to ClipRect with the translated portRect as an argument is
appropriate after the SetOrigin call.
PopUpSelect accomplishes the task of saving the part of the screen under the
pop-up by copying that part to an off-screen bitmap using the Quickdraw routine
CopyBits. The off-screen bitmap is allocated with a NewHandle call. The width of the
menu rectangle (plus the width of the frame and shadow) determines the value of its
rowBytes field (appropriate rounded up to a multiple of 2 bytes, since rowBytes must
be even). The height of the menu rectangle (plus the height of the frame and shadow)
determines the number of rows in the bitmap. These two values multiplied together,
plus the size of a bitmap structure, determine the number of bytes to allocate. The
bitmap structure itself is build in the first few bytes of the allocated area. The
baseAddr field of the bitmap points to the byte immediately following the end of the
bitmap structure.
The next task, that of drawing the menu, is performed by calling the standard
menu definition procedure whose handle is contained in the menuProc field of the menu
record. A menu definition procedure has the following interface:
#define mDrawMsg 0
#define mChooseMsg 1
#define mSizeMsg 2
MenuDefProc(message, theMenu, menuRect,
hitPt, whichItem)
int16 message;
MenuHandle theMenu;
Rect *menuRect;
Point *hitPt;
int16 * whichItem;
Passing the message mDrawMsg to the menu definition procedure causes it to
draw the menu inside the rectangle menuRect. The Window Manager port should be the
current port when this message is sent. I set the port's clipRgn equal to menuRect, just
to be safe.
The task of tracking the mouse is accomplished by passing the message
mChooseMsg to the menu definition procedure. When the procedure receives
mChooseMsg, its hitPt parameter is the current mouse location, and its whichItem
parameter is the item number of the last item that was choosen from the menu. If the
location is in an enabled menu item, the procedure unhighlights whichItem and
highlights the newly choosen item (unless the new item is the same as the whichItem),
and returns the item number of the new item in whichItem. If the location isn't in an
enabled item (or isn't inside menuRect), the procedure unhighlights whichItem and
returns 0. The following fragment of code shows how this works:
whichItem = 0;
SetPt(hitPt, 0, 0);
do
MenuDefProc(mChooseMsg, theMenu,
&menuRect, hitPt, & whichItem);
GetMouse(hitPt);
}
while (WaitMouseUp());
WhichItem and hitPt are both initialized to 0 since the local coordinate system
has changed, invalidating the original value of hitPt. This initialization is consistent
since the local origin is outside menuRect. GetMouse returns the mouse location in the
local coordinate system of the current port, which is just what we want. Finally,
WaitMouseUp is called rather than StillDown so that the pending mouse-up event is
removed, to avoid confusing the application (MenuSelect does this too).