The Screen Machine II
Pull-Down Menus In IBM BASIC
Charles Brannon, Program EditorLast month we presented "The Screen Machine II," a full-featured drawing program for the IBM PC and PCjr, Pull-down menus make it quick and easy to use. Many programmers would like to add user interface tools like pull-down menus to their own programs, so this month we'll take a look at the techniques used in Screen Machine II. The programs require an IBM PC with color/graphics adapter and BASICA or a PCjr with Cartridge BASIC. A joystick or graphics tablet is optional but recommended.
"The Screen Machine II" is a powerful graphics program that lets you draw in full color with a complete set of drawing tools. It is designed to be as easy to learn as possible without encumbering advanced users. Last month in Part 1, we listed Screen Machine II without REMarks for the sake of brevity. This month, we're publishing the fully commented version with an explanation of how you can use the menu subroutines in your own programs. See Part 1 for an explanation of how to use Screen Machine II.
Why A Visual Interface?
The visual user interface—consisting of pull-down menus, icons, and screen windows—is rapidly becoming the most popular way to operate a personal computer. Since the Apple Macintosh was introduced in 1984, nearly a million Macs have been sold. The basic principles have been adapted by the Atari ST series and Commodore Amiga, and several similar shells are available for IBM PC computers—including Digital Research's GEM, IBM's Topview, and Microsoft's Windows. Even the older eight-bit computers, such as the Commodore 64, are being updated with visually oriented operating systems like GEOS.
Those who prefer this style sometimes say that the best advantage of the visual interface is that it makes you feel as if you have a tangible presence within the computer. Instead of viewing yourself as a somewhat remote user of the machine, an operator at a terminal, you feel more like a part of the system. Your sense of flow is enhanced because you can instantly recognize graphic metaphors (such as a picture of a disk) or simply scan through pull-down menus to see what commands are available and appropriate.
A drawing program that takes advantage of this approach lets you preview the figures you're drawing before you actually set them in stone (or phosphor). For instance, using a mouse controller, joystick, or graphics tablet, you can move the pointing arrow across the screen canvas, then click a button to set one endpoint of a line. Now, as you move the pointer, a "rubber-band" line is drawn between the first point and the current cursor position. You can move the line around, changing its orientation and length, until you've put it right where you want it. Then you press a button again to stamp it down. Of course, if this still isn't what you want, an Undo command could restore the screen to its former state.
If you've never had a chance to work with pull-down menus, you might not appreciate their advantages. Since the menus let you both view and execute the program's commands, they serve two functions: They provide a way to use the program while acting as built-in documentation. Menus that drop down from the top of the screen let you work with nearly the full screen area, instead of cluttering it up with help screens or conventional menus.
On the other hand, if you prefer a written approach to communication, you may find the act of scurrying around a dynamic screen to be clumsy and inefficient, particularly if you have little trouble memorizing lots of commands and typing at least 30 words per minute. A program that seeks to keep everybody happy can provide alternative keyboard commands as well as menus and icons.
Writing a program which incorporates a visual user interface can be tricky. The newest Microsoft BASICs—such as Microsoft BASIC for the Macintosh and Amiga BASIC— have built-in commands to create and manage pull-down menus. Creating a menu is as simple as listing the text in a series of MENU statements. There are even ON MENU GOSUB statements which set up event traps (BASIC interrupts) to detect menu selections. Other commands, such as ON MOUSE GOSUB, let the program read the pointing device and respond to button clicks.
IBM BASIC lacks these features, but does include event-trapping statements like ON STRIG GOSUB for the joystick. This makes it possible to simulate the operations which are handled automatically by the newer BASlCs. When the user clicks the selection button on the pointing device, the program has to check to see if the pointer is within the menu bar (the first line of the screen). If so, it then checks to see if the arrow is pointing at one of the menu titles. If so, the program drops down the menu (saving the screen contents of the area overwritten by the menu box), and again checks the pointer position to see which menu item is being pointed at. The program highlights the item, and then un-highlights it if the pointer moves away from the item, Finally, when an item is selected (or when the menu selection is canceled), the program has to remove the menu from the screen, restoring the screen contents overlapped by the menu.
Again, all of these details are handled for the programmer in Macintosh and Amiga BASIC. Nevertheless, with enough programming, you can do the same thing in IBM BASIC. The key is being able to drop down a menu and then later restore the part of the screen overlapped by the menu.
BASIC'S GET and PUT commands are the solution: GET is used to copy a rectangular portion of the screen into a storage array, and PUT copies the image from the array back to the screen. Naturally, this technique requires using a graphics mode, since you can't GET or PUT with the text screen. However, with a machine language routine to buffer part of the text screen, this method could be adapted for use with a text-only display adapter.
Simulated MENU Commands
Screen Machine II demonstrates how this technique works. It contains several subroutines which simulate the MENU commands and event traps found in Macintosh and Amiga BASIC. Fortunately, you don't have to know about the inner workings of these subroutines to use them in your programs. There are a few variables and arrays that need to be defined (some of these are initialized automatically), but you need only three GOSUBs to do everything:
GOSUB 11000 is used to add a menu title or menu item to the list of menus.
GOSUB 14000, used within a loop, tracks the arrow pointer and continually checks for a menu selection. If a menu is selected, it automatically handles the mechanics of dropping down the menu, getting a selection, and then restoring the screen. You then examine the variables MNID (menu id) and MNIT (menu item) to see which, if any, menu item was selected.
GOSUB 20000 reads the pointer position and optionally tracks the cursor automatically.
Essentially, these subroutines are substitutes for the MENU command, MENU function, and MOUSE function built into Macintosh and Amiga BASIC. Therefore, they can be very handy for translating Macintosh and Amiga programs into IBM BASIC.
A few other useful subroutines let you turn the cursor on or off and print text on the graphics screen in reverse-video. All of these routines let you set variables to allow special options or fetch additional information. Most importantly, they are designed to be used with any program, not just Screen Machine II, so you can easily add them to an existing program or use them as a starting point for your next project.
Screen Machine II is far too large to cover in detail, but the listing below (Program 1) is liberally commented with REMs. By following these comments you can easily deduce the flow of the program. If you didn't type in the program fast month, you can enter this listing and omit the comments without ill effect. (Aside from the remarks, this month's program is identical to last month's.) In fact, the remarks take up too much memory to allow the program to run. If you type in the program as listed, use Program 2, "REMover," to remove all the remarks to create a runnable version.
REMover can be used to strip comments from other IBM BASIC programs, too, When you run REMover, first enter the name of the program you're deleting the REMs from, followed by a unique filename for the REMless program to be created. You then have two options. Option 1 changes all REM statements into a single apostrophe (the abbreviation for REM). This preserves the line in case it is the target of a GOTO or GOSUB (not a problem with Screen Machine II), but deletes the text of the remark. Option 2 deletes all REM or apostrophe statements, and if the REM is the only statement in the line, deletes the entire line as well. It's not safe to use Option 2 on programs that may branch to a line beginning with a remark, but it works just fine with Screen Machine II. Be sure you keep a copy of your unprocessed, remarked program for future reference.
Using Menus In Your Program
You can detach the menu package from the rest of the program either by deleting everything except lines 10000-21040, or by saving just the menu lines to disk as an ASCII file suitable for merging with another program, Just enter to create an ASCII file on disk called MENU.PAK. You can then type MERGE "MENU.PAK" to add these lines to an existing program. If you are starting from scratch, type LOAD "MENU.PAK".
Before your program can call the menu package, you need to initialize certain variables. These variables are shown in lines 210-340 of the Screen Machine II listing. See the section on GETXY below to see how to set ACC, DACC, FROZEN, XMAX, YMAX, XOFF, and YOFF. Check the section on CURSOR— ON and CURSOR_OFF for information on setting the CURSOR flag. Finally, you can choose sound effects by setting SNDFX to -1. If you set it to zero, no sound is used.
Lines 9000-9340 illustrate how to define your menu structure. For example, the DATA statements for the Picture menu are
DATA 1,0,1, "Picture"
DATA 1,1,1, "Undo U"
DATA 1,2,1, "New ^N"
DATA 1,3,1, "Open O"
DATA 1,4,1, "Save S"
DATA 1,5,1, "View V"
DATA 1,6,1, "Quit ^Q"
The first number is the menu-ID, the number specifying which menu is being defined. It must be at least 1, and less than 9 (unless you change line 11000 to allow more than 8 menus and/or more than 8 items in each menu). The next number is the menu item, A menu item of 0 defines the title of the menu, and other numbers specify the name of each item within the menu. The next number is a status flag for that menu item. A value of 1 is normal. Use 2 to display a checkmark next to an item.
The Ghost In The Machine
For example, the Tools menu puts a checkmark next to the currently selected tool. This allows a menu to be used to select items, show which commands are available, and show the status of each menu item.
When you specify a value of 0 for the menu status flag, that menu item is ghosted out, or dimmed. A ghosted item is still readable, but the text is distorted, indicating to the user that this particular command is currently disabled or not appropriate at the current time. This helps users avoid confusion over what they can and cannot do in a given situation—they can always access a command unless it's ghosted out.
There are many times when a program would want to change these assignments, depending on program context. For instance, after you select a new tool, the previous tool is reset to a flag of 1 (normal), and the new item is set to 2 (checked). In the Preferences menu, some of the menu items—such as Bkgd Color—are ghosted out when you are in 640 X 200 mode (because you can't change the screen color in this mode), but revert to normal when you select another graphics mode.
Here are descriptions of all the major routines in Screen Machine II:
11000 MENU To initialize or change the value of a menu item, assign values to the variables MNID, MNIT, and MNSTR$, then GOSUB 11000. MNID holds the number of the menu (1-8); MNIT holds which menu item is being changed (0-8, where 0 is the menu title); and MNSTR$ is the text displayed as the menu title or menu item. A program can modify all of these items at any time, changing the appearance of the menu when it drops down.
The subroutine at line 9000 in Screen Machine II can be used as a model for initializing your own menus. This routine stores the values in the arrays MTITLE$, MFLAGS, and MITEMS. It stores the number of the highest menu-ID used so far in TOPID to find out how many menus there are. The one-dimensional array MITEMS holds the number of menu items in each menu. MTITLE$ and MFLAGS are two-dimensional arrays that use MNID and MNIT to point to the title string and flag setting for a menu item. Hence, MFLAGS (1,2) holds the status flag value of menu 1, item 2. MTITLE$(3,0) holds the title of the third menu.
It can be convenient to assign values to these arrays directly—for example, when you just want to change one menu item's status flag. MFLAGS(3,4)=0 would ghost out the fourth item of the third menu. You could change it back to normal with MFLAGS(3,4)=1. Or you might want to change the text of a menu entry by modifying the MTITLE$ array. For instance, a menu item could initially read SOUND ON, then change to SOUND OFF after you've turned on the sound. This is an alternative to using the checkmark, but it can be confusing. Does SOUND ON imply that the sound is already on, or that the item will turn on the sound? Most programs use checkmarks to avoid this confusion.
12000 MENU_REFRESH Use GO-SUB 12000 to display the title bar of your menus after you've initialized them after successive calls to the subroutine at line 11000. Your program should try to avoid using the top line of the screen, but you can always use GOSUB 12000 to redisplay the menu bar if the top line is lost. This routine also links in the positions of each menu item so that the MENU_POLL routine (line 14000) can figure out which menu you are pointing at. These positions are stored in the MX array.
13000 RVSMSG$ There is no easy way to print reverse-video text on the IBM graphics screen, but this is the effect we want when we highlight a menu title or menu entry. The menu bar is also printed in reverse. To display reverse text, set MSG$ to the text you'd like to PRINT, then GOSUB 13000. This routine prints the text, uses GET to copy the text into an array, then uses the PRESET option of PUT to stamp down a reverse copy of the text.
14000 MENU_POLL This is the workhorse of the menu package. When you call this routine, it checks to see if the pointer is pointing at a menu title and the button is pressed. If not, it just RETURNS, leaving the variables MNID and MNIT set to 0. Otherwise, it drops down the menu, gets the selection, and exits with MNID and MNIT set to the value of the menu-ID and menu item. If the user canceled the selection by moving outside of the menu box, MNIT and MNID are reset to 0.
This routine uses simple sound effects as additional audio cues for the user. If you set the variable SNDFX to 0, you won't get sound effects. If you want them, set SNDFX to -1.
This routine also preserves your screen display and cursor position. If the keyboard is used for menu selection, the keyboard offset (see below) is increased to speed up movement between menu items.
Be aware that this routine works like INKEY$—if there is no menu selected yet, it immediately RETURNS. You need to continually call this routine within a loop until MNIT is nonzero, meaning that a menu has been selected. The cursor arrow is updated automatically throughout the menu selection process. Even if no menu is selected, calling this routine continually calls the GETXY routine at line 20000 to update the cursor position.
15000 This subroutine is used only by MENU—POLL to flash a selected menu item.
16000 MENU_DOWN Given a value in MNID, this routine drops down the indicated menu, saving the screen contents erased by the menu in the MSAVE% array (initialized in line 11010). This routine is really only called by the MENU_POLL routine when a menu has been selected, but you may be able to use it for some special effects. To remove the menu, be sure to use the next routine, MENU_AWAY, to discard the menu and restore the screen contents.
17000 MENU_AWAY Again, this is really only used by MENU_POLL to roll away the menu after the user has made a selection. You can use it to remove the menu and restore the screen if you used MENU_DOWN to drop the menu yourself.
19000 CURSOR_OFF The arrow pointer is defined in this program in the subroutine at line 3000, used to select various graphics modes. You could excerpt line 3050 (as long as you remember to DIM ARROW% (32) at the start of your program) to use this cursor in your own program. Otherwise, draw your cursor on the screen and use GET (x1, y1)-(x2, y2), ARROW% to copy the cursor into the ARROW% array (x1, y1, and x2, y2 are opposite endpoints of an imaginary rectangle that should completely enclose the cursor shape). The GETXY routine (20000) needs to know the width and height of the cursor, so store these values in XARROW and YARROW.
The cursor is animated with the XOR option of PUT. When you PUT the arrow, it combines itself with the existing screen display so that it is always in contrast. Just think of the cursor as a stamp that uses "negative ink"—ink that reverses the color of anything it touches. For example, a white arrow on a black background would be white, but on a white background would be black. The magic of XOR is that when you PUT the shape back down on top of itself, it reverses the action, removing the arrow and restoring the previous screen contents. Although PUT with XOR can be flicker-prone, you can reduce the flicker by increasing the delay between drawing the arrow and erasing it.
You don't have to worry about updating the arrow cursor yourself. As long as you continually call either MENU_POLL (14000) or GETXY (20000), the arrow position is updated while the routine is checking the pointer position. But you have to remember to remove the arrow from the screen before you draw anything that might overlap the arrow. If you drew a white line through the cursor while it was resting on a white area, you've drawn a white line through the black arrow. When the arrow is PUT back on top of itself to erase the arrow, the conditions are no longer the same. The cursor reverses itself, so the cursor is gone, but you're left with a black line where the cursor used to be (remember the "negative ink" analogy).
Therefore, your program needs to erase the cursor from the screen before drawing anything. After you've drawn your figure, you can turn the cursor back on, or just allow GETXY (20000) to turn it back on automatically the next time you check for the cursor position.
So use GOSUB 19000 to turn off the cursor, and GOSUB 18000 to turn it back on. This is not the same as setting the CURSOR flag (see GETXY below). The CURSOR flag prevents or enables automatic cursor updates, but doesn't graphically affect the display. However, you should turn off the cursor with GOSUB 19000 before you turn off the cursor flag. If this seems confusing, examine the drawing routines in Screen Machine II (lines 1000-1660) to see how this is done.
20000 GETXY This routine is the core of the whole package. It is used any time a routine wants to know where the cursor is pointing. As part of the normal checks for the joystick position, it can also update the cursor automatically. To get automatic cursor tracking, be sure to set the CURSOR flag to -1; otherwise you are responsible for your own cursor movement. For use with a joystick or graphics tablet, this routine converts the joystick/tablet values to actual screen positions by multiplying the controller position times the values XRATIO# and YRATIO#.
XRATIO# and YRATIO# are the horizontal and vertical size of the screen divided by the maximum X and Y values of the controller (the lower-right position). When multiplied by the joystick value, these values scale the joystick values to actual screen coordinates. A range of 0-255 multiplied by 1.251 (319/255) gives us a range from 0-319.
Set XRATIO# to the horizontal size of the screen divided by the maximum value of the controller. If the maximum value of the joystick is 132, and you're working with the 320 X 200 mode, then XRATIO# = 320/132. Similarly, YRATIO# is the number of rows divided by the maximum vertical position of the controller, as in YRATIO# = 200/130.
Reading The Pointing Device
XOFF is the minimum horizontal value of the joystick, and YOFF is the smallest vertical value returned by the joystick. You can test this by pushing the joystick to the upper-left corner, then executing PRINT STICK(0), STICK(1). Similarly, you can move the joystick to the lower-right corner and PRINT STICK(0), STICK(1) to assign default values to XMAX and YMAX as shown in lines 230 and 240. Screen Machine II illustrates how to set these values in the screen setup routine at line 3000. Also, the Calibrate function from the Preferences menu (refer to lines 2440-2510) is used to read the values of XMAX, YMAX, XOFF, and YOFF.
XOFF and YOFF, the minimum (top-left) values of the controller, are used to adjust the calculations, as well as to check whether a stylus is pressed against a graphics tablet surface, For example, the Koala-Pad usually returns 7 and 7 as its X and Y values when there is no surface contact. This can be used as a convenient shortcut. While in drawing mode, for example, you start drawing by clicking the button, and stop drawing by either clicking the button again, or simply moving the stylus off the tablet surface.
Another note about the Koala-Pad: It is extremely sensitive to glitches unless you bear down on the tablet with firm pressure. Unfortunately, pressing too hard will score the tablet surface. If you don't press hard enough, the values jitter uncontrollably. Fortunately, BASIC is too slow to notice most of these glitches, which occur for a fraction of a second before the values reset to normal. If you compile the program, though, it is much more sensitive to these glitches. An averaging routine could be used to detect the glitches and ignore them, but would greatly slow down the uncompiled program.
For keyboard control, GETXY allows the cursor keys to be used to move the cursor. If the keyboard was used instead of the controller, the variable KEYMODE is set to -1; otherwise KEYMODE is reset to 0 when the joystick or graphics tablet is used.
Cursor movement can be very slow, though, if you are moving only one pixel at a time. You must set the variable DACC to the number of pixels you'd like the pointer to move each time a cursor key is pressed, and initialize the variable ACC to this value. If the key is pressed successively or held down until it repeats, ACC counts up, accelerating the speed of the arrow cursor. When the key is released or a different key is pressed, ACC is reset to the value of DACC.
On the other hand, if DACC is a negative quantity, no acceleration is performed. Every keypress just advances the cursor by the absolute value of DACC (as if it were positive). You can change these values throughout your program depending on the context. The MENU_POLL routine sets DACC to —8 during menu selection so that the cursor keys move by one screen line at a time without accelerating.
Reading The Keyboard
If the flag FROZEN equals -1, the joystick or graphics tablet is ignored in favor of the keyboard. Do this when you need keyboard control while the joystick is plugged in. Although the keyboard is always active, it attempts to increment or decrement the values of MX and MY, but these variables are continually reset to the scaled value of the joystick position. With the graphics tablet, we can tell if the stylus is pressed down and ignore the tablet position if it isn't. So the keyboard and tablet work interchangeably, but you need to set FROZEN to —1 if you want keyboard control only while ignoring the joystick.
Line 20050 checks for keyboard equivalents that indirectly activate menu entries. Most commands in Screen Machine II have keyboard equivalents—O for Open, L for Lines, CTRL-N (N) for New, etc. In addition to streamlining the program for advanced users, keyboard commands satisfy those who are uncomfortable with pointing and clicking. If you don't mind memorizing every keystroke, you don't really need menus. However, not every menu item is always represented by a keystroke, and it's hard to find unique assignments for every menu item.
You really don't need to bother with keyboard equivalents, but if you want them, initialize the string CM$ as illustrated in line 9060. For each keyboard equivalent, include the keyboard character followed by the digit of the menu-ID and the digit of the menu item for that menu selection. This limits you to nine menus and items, but makes keyboard checking quick. INSTR$ is used to instantly find out if the command key is part of CM$, and just as easily retrieve the subsequent values of MNID and MNIT. Strictly speaking, this line does not really belong in GETXY, but we need it here to use the same keystroke that GETXY uses to check for a cursor key.
Study the program listing for more ideas. Since nearly every line is commented, it should be easy enough to follow. We would be interested in seeing the kinds of programs you develop using these techniques.
Quick Reference To Subroutines
Uses MNID, MNIT, and MNSTR$ to initialize a menu item.
MNID: Which menu
MNIT: Which menu item
Fills the arrays MTITLE$( ), MFLAGS( ), MITEMS( )
Diplays MSG$ in reverse video at current cursor position. )
If a menu item is found, returns menu-ID in MNID and menu item in MNIT; otherwise MNID = 0 and MNIT = 0.
If the cursor flag is set (CURSOR<>0), draws pointer cursor and tells the package that the cursor is on the screen by setting TOGGLE=1.
If the cursor flag is set (CURSOR<>0), removes pointer cursor from screen and tells the package that the cursor is not on the screen by setting TOGGLE=0.
Polls keyboard and optionally the joystick (if FROZEN=0). See text for necessary initialization. Returns MX, MY, MB (mouse/joystick position and button status). If CURSOR flag is nonzero, automatically updates an arrow cursor at position MX, MY.