Leor Zolman has been involved with microcomputer programming for 15 years. He is the author of BDS C, the first C compiler targeted exclusively for personal computers. Leor's first book, Illustrated Co, is now available from R&D Publications, Inc. Leor and his family live in Lawrence, KS.
This month we continue to dissect rmenu, the runtime interpreter for the CMENU system. We'll be looking primarily at the code that handles the full-screen user interface.
Command Processing
Most of rmenu's user interface code appears in rmenu2.c (Listing 1) , divided between the sub_menu and get_cmd functions. The get_cmd function accepts keystroke commands and maps them onto a standard K_ code (as defined in the rcmenu.h header file, May 1992, Listing 1) . sub_menu acts upon those command codes. Some additional user interface code appears in the do_cmnd function (next installment) when a prompt is required after an item's execution, but before control is returned to the menu navigation functions.
Loading a Menu File
The ld_menu function (Listing 2, line 98) reads an .mnc file in from disk, allocating memory as necessary to hold the menu data and determines the precise way each menu item shall be positioned on the screen. All this is performed during one pass through the disk file.The first thing ld_menu does is initialize a convenience pointer named Levp (line 111) to simplify access to the LMenus structure representing the current nesting level. All operations performed by this instance of ld_menu will be confined to the current nesting level. There is no reason to require the repetitive evaluation of a base expression such as
LMenus[nestlev]when it can be evaluated once and assigned to a pointer. All the other pointers defined at the top of ld_menu serve a similar purpose later in the function.After the input file has been opened successfully, ld_menu is ready to read in the data. First, the integer value telling the number of menus present in the file is read from the beginning of the file (lines 123-124). For portability reasons, the sizeof operator is used to determine the size of an integer variable.
The main loop (lines 127-209) executes once for each menu in the file. If the memory for each new menu's index position in the LMenus array has already been allocated, then the M2p pointer is simply assigned to point to that memory. If not, lines 132-138 perform the necessary memory allocation, and initialize the memory tracking variables max_menus (in the LMenus structure) and most_items (in the Menus structure for the new menu). These two variables track, respectively, the maximum number of menus loaded into a nesting level and the maximum number of items loaded into a particular menu. They retain their values as menus and items come and go dynamically.
When menu memory allocation issues have been settled, the MENU header structure for the current menu is loaded in lines 142-143. The menu item placement strategy is then computed.
Laying It All Out
The algorithm to calculate item positioning is complex, because of the number of variables involved. These variables include:1) screen dimensions currently specified by symbolic constants, (but could as easily be specified by a set of variables see the exercises for some ideas along this line);
2) explicit columns and spacing instructions appearing in the menu specification file;
3) the number of items in the menu;
4) an arbitrary default heuristic for arranging the items in the absence of explicit columns or spacing specifications.
The placement function starts the process by examining the general characteristics of the entire menu and adjusting the values of the columns and spacing variables accordingly. Because cmenu provides the length of the menu's widest line of item text (in the MENU element named, appropriately, widest), placement has all the information it needs in the MENU header and does not need to examine the individual items to establish the overall placement strategy.
The MENU elements placement examines are: spacing, columns, nitems, and widest. Because the first three of these elements are examined repeatedly in placement, I've assigned their values to namesake local variables (lines 224-226) to make the code easier to follow.
The first step of the placement algorithm is to fill in any missing values for spacing and columns. There is one case when neither are specified, one case when only spacing is specified, and one case when only columns is specified. Each of the three cases is handled individually.
When neither value is specified, the placement is based on the relationship between the number of items in the menu and the number of lines available on the screen for menu items. If the number of items is less than half the number of lines, we are free to use double spacing to fill up the screen, putting a blank line between each item line for enhanced readability.
If the number of items is greater than half, but less than or equal to the number of rows available, then we examine the length of the longest item to see if it would fit in half a screen width. If so, then we go to a two column format to allow double spacing. If the longest item is too long for a two column format, we stick with one column and go to single spacing.
If the number of items is greater than the number of rows, then single spacing is forced and the number of columns is set to whatever it takes to make room for all the items. If that means some of the item text might be truncated, well, too bad. You can always put additional information for any item into its help clause, which is guaranteed never to be truncated (since there is always a full line available for each item's help text).
If only an explicit columns value is given, then lines 250-251 calculate the spacing appropriate for that number of columns (either single or double).
If only an explicit spacing value is given, then lines 253-256 figure the best number of columns for that spacing value. No adjustment is made for items that might be truncated when double spacing is specified and more items exist than would fit in a single column. If the menu designer really wants double spacing, why should rmenu second-guess?
After the spacing and columns values are filled in, one more step is needed to guard against the case where an impossible configuration was specified (such as a column value of one, with a hundred items following). If necessary, the code in lines 260-264 reduces the spacing and increases the number of columns (in that order) until a proper fit is achieved. Control then returns to ld_menu.
Back in ld_menu, it is now time to read in the individual ITEM structures from the disk file. Armed with reasonable values for spacing and columns, the length of the uniform item fields is calculated and stored in the variable named field_len (line 149). This helps optimize screen refreshing speed. If the longest actual item text is shorter than the available space for each item, there is never any need to write into the region between the end of the item and the start of the next item to the right of it (or the end of the screen, if we're dealing with only one column).
Each pass through the loop in lines 155-208 reads in one menu item. Again, an incremental memory allocation strategy is employed. The code to perform the memory allocation is similar to that used to obtain memory for the MENU structures (lines 133-137), except for the names involved (and one less initialization).
The ITEM structure itself is loaded in line 167. Then, the item text is truncated to fit the available display field length.
The next section of code is a bit tricky. I thought it would be useful if menu items that select other menus were clearly marked as such. The description text for those items wouldn't need to state that the selection is a menu. The way I chose to implement this feature was to have rmenu insert the text "(MENU)" at the end of each line of item text that identifies a submenu (local or external). Additionally, all such identifiers are inserted at the same column position relative to the start of the item text. Thus, screen items keep an orderly appearance.
Where there isn't enough room left in an item's text description buffer for the six characters of additional text, no action is taken. Nothing inherent to the display algorithm will distinguish that particular item as a submenu selection (although the menu designer is certainly free to place such descriptive text in a help line for the item.)
The bulky if expression in lines 172-174 checks if the current item specifies a submenu, and if there is enough room for the additional text. If so, lines 175-184 insert the appropriate number of spaces after the end of the item text, then append the "(MENU)" text after the spaces. Note the assignment of the horizontal position to the variable limit in lines 178-179. The expression
M2p -> field_len - 7represents the furthest to the right that the menu identifier could go to keep the combined text length within the established field width. It may not look good all the way to the right, however, if all the description texts are short. The limit value, therefore, is calculated as the minimum of the expression above and the length of the widest item plus two (to leave some breathing room). Care also must be taken not to write past the end of the char array that holds the description text; a condition to test for such an overflow is part of the loop that inserts the space characters into the text (line 181). I warned you this would be tricky.The final calculation required for each item is to establish:
1) its text description's screen coordinates, and
2) the number of spaces needed to pad its text out to the uniform field length.
All these values are then stored in the coords array position for the item. The screen coordinates are calculated in lines 186-202 based on the spacing (single or double), the number of physical screen rows devoted to item descriptions (MAX_IROWS), the physical number of columns on the screen (SCREEN_COLS), and the coordinates of the top-left corner of the item description block (HOME_Y, HOME_X).
The calculation of the item text's horizontal position xpos (lines 191-202) merits some clarification. This value is computed differently depending on whether the items are being displayed in a single column or in multiple columns. For a single column, the horizontal position is centered on the screen (lines 196-197); otherwise, the text is left-justified within each column (lines 200-201). The "magic numbers" in line 197 result in a fairly consistent, attractive centered layout on an 80-column screen.
The number of spaces needed to pad out the text is calculated by the expression in lines 206-207. The set of above calculations is complex, but it is only performed once. After that, the resulting values are all stored directly in the coords data structure for rapid access during the screen refresh process.
Real-Time Menu Processing
After all the preliminary steps have been completed, the sub_menu function (this month's Listing 1) is finally given the chance to run a menu for the user.There are some state variables that sub_menu maintains:
cur_item holds the index value of the currently highlighted menu item. It is initialized to zero for the first item in the menu. The index value of an item is always one less than the reference number displayed on the screen for that item. C programs like to count from zero, but most people prefer enumeration to begin with one.
sel_val represents the latest direct numeric item selection value entered by the user via the digit keys. It is directly set by the get_cmd function whenever the user enters a numeric selection value, indicated by a return value of K_DIRECT from get_cmd calls.
To display the menu on the user's screen, sub_menu calls the draw_menu function in rmenu2.c (Listing 1, line 173).
draw_menu begins by centering the menu title text on screen line TITLE_ROW (lines 190-191). This represents our first glimpse of a Curses display sequence. The move function positions the logical cursor to the specified row and column, and addstr inserts a given text string at the current cursor position. The text does not immediately appear on the screen, however, because the program has not yet called the refresh function.
The Curses library supports multiple windows. When the library is first initialized via a call to the init_win function, one default window corresponding to the regular terminal screen is automatically opened. Most of the more common functions in the Curses library have two variations: one always acts upon the "default window," and one takes an additional parameter specifying a particular window on which to operate. Because we will always be dealing with the default window for this application, the shorter forms are used whenever available. (Actually, the shorter forms are implemented as macro definitions that expand to call the long form functions. They are not separate functions themselves. For the purpose of writing programs, however, they may be treated as shorter function calls.)
Drawing the Menu and Its Items
The loop in lines 193-195 calls the draw_item function (lines 212-276) to write each individual item to the default window. The currently selected item is written with instructions to use reverse video and display any optional associated help text in the help line area of the screen. All other items are written in normal video mode with help text suppressed.After all items have been written, the prompt line is constructed on row PROMPT_ROW. It is constructed because it takes on one of two different appearances, depending upon the current value of the escape flag in the current menu's header. This flag tells whether the user is permitted to use the ! command to invoke a command processor (shell) directly from within the current menu. If shell escapes are allowed, then the fragment of text that documents the option is included in the prompt string; if not, that text is not included.
The last thing draw_menu does is call refresh to cause Curses to update the screen with all the changes.
The draw_item function begins by moving the logical cursor to the location stored in the coords structure for the current item (line 229) and writing the item number there using the Curses function printw (the item number is always written in normal video mode). If the vid_mode parameter specifies to use reverse video, then the Curses function standout is called to switch screen writes into the standout video mode. The precise effect of calling standout may vary from system to system and terminal to terminal (at least in the UNIX versions), because the sequences used are terminal dependent. On most terminals, however, you can expect standout to enter reverse video mode.
The item text is then written to the logical screen (line 235), along with the appropriate number of spaces needed to fill out the total length of the item field. If standout mode was requested, then the standend function is called to return to normal video mode.
If the dohelp parameter was specified (i.e., has a nonzero value), and the current item has help text associated with it, then that help text is displayed in standout mode (lines 247-268). If the help text is not too long, then some extra blank padding is added at both ends of the line to improve readability in standout mode. If there is no help text, then the help area is cleared (lines 271-272).
Back to sub_menu
Upon return from the draw_menu call, sub_menu constructs the new default incremental path via a call to the make_path function and stores the returned result in the newpath string. (I'll describe make_path more fully in a later section.)The main user interface loop (lines 40-169) takes up the rest of the sub_menu function. get_cmd is called at the top of the loop to read a keyboard command from the user. The return value from get_cmd indicates the nature of that command. All commands except direct item selection (by number) are entirely defined by the value of the command code returned. For direct item selections, the return value of K_DIRECT indicates that such a selection has been made. The number of the selected item is stored in the sel_val variable.
All the simple highlight bar movement commands are processed in lines 44-86. The up-arrow and down-arrow commands (and the space bar command, which is equivalent to down-arrow) are the easiest to process. The currently highlighted item is redrawn in normal video, the new item is selected by either incrementing or decrementing the cur_item variable (with wrap-around), and the new current item is redrawn in standout mode. Processing the direct item addressing command is just as easy (lines 121-128), except a test is thrown in that skips the redraw operations if the item selected is already the current item.
The left-arrow and right-arrow keys are trickier, because the index number of the new item must be calculated based on the physical arrangement of items on the screen. For example, when the right-arrow key is pressed, there may or may not be an item in the column to the right (in fact, there may not even be a column to the right).
Lines 64-68 and 78-83 compute the next item index in response to a right-arrow and left-arrow command, respectively. To achieve this feat, the algorithm considers the total number rows in the item display area (MAX_IROWS), the current spacing value, the total number of items in the menu, and the index value of the current item. In trying to get these few lines of code to work correctly, I experienced almost as much frustration as I did with all the rest of the CMENU-related design problems combined. I finally solved this by calculating the maximum number of items that can fit into a single column (a value I called the "factor"), and testing for overflow by adding or subtracting that factor from the current item's index value.
When moving to the right, the new index is computed by adding the factor to the current item index. Either the resulting index is within the range of menu items (the case when an item to the right exists) or it is out of range (then the index corresponding to the item on the same row in the first column is calculated by computing the modulus of the original index and the factor).
When moving to the left, starting anywhere other than in the first column, the new item is simply in the column to the left. When starting in the first column, however, a loop is executed to find the index of the item in the right most column of the same row.
When get_cmd returns KEY_RUN, then the action associated with the current item is executed. If the action code value is ACT_EXIT, sub_menu is finished and returns with a value of OK to indicate no problem.
Any other type of action is passed to do_item for processing. do_item is discussed in a later section. For the time being, note that a fatal error might be encountered during the do_item call. do_item indicates a fatal error condition by returning a value of EXITALL, causing sub_menu to immediately return to its caller with EXITALL as its return value.
If the do_item call went smoothly, lines 98-116 determine the next menu item to be highlighted, according to the options specified in the menu source. The menu is then rewritten and control returns to the top of the main command loop.
Lines 130-152 process the shell escape command. If shell escapes are currently disabled, the put_msg function is called to display an error message and wait for the user to press a key to return to normal menu processing.
If shell escapes are enabled, then there are two possible methods of dealing with the request, controlled by the symbolic constant SHELL_PROMPT.
If SHELL_PROMPT is true, then a special prompt string (SH_PROMPT_STR) is displayed, again employing the put_msg function to show the message and wait for a keystroke. If the user presses the ESC key in response to this prompt, then the shell escape request is aborted. If any other key is pressed, the shell escape is performed. If SHELL_PROMPT is false, no prompt string is displayed, and the shell escape is performed immediately. This may be thought of as expert mode, when there is no danger of confusing the user if the shell escape command key is pressed accidentally.
Lines 144-146 manage the invocation of a subordinate shell. A pair of functions, pre_shell and post_shell, together handle certain housekeeping tasks attendant to launching a subordinate command interpreter from within the Curses environment. The system call that actually launches the shell is sandwiched in between these two functions.
The last four cases in sub_menu (lines 154-166) handle various forms of the exit command, display the program version number, or make a rude noise when an unrecognized key is pressed.
Keystroke Processing
At the lowest level of the user command processing loop, get_cmd reads user keystrokes (lines 279-386). get_cmd translates raw keystrokes into a set of standard command symbols and returns those command symbols. For most commands this involves a simple one-to-one mapping of keys to symbols.get_cmd also performs another somewhat trickier task. It processes any direct menu item numbers entered by the user, and manages the contents of the item number echo area at the far right portion of the prompt line where digits are echoed during direct item number entry.
Two parameters are passed to get_cmd: nitems tells how many items there are in the current menu, and curr is the index of the currently highlighted item.
get_cmd maintains a static Boolean variable named digits that tells if a string of digits (presumably representing a direct item number) are currently being entered. Since each keystroke is an individual command, the entry of a multiple digit item number must necessarily span several calls to get_cmd.
The first thing get_cmd does is display the item number of the currently highlighted item in the echo area (lines 301-302). The item number is one greater than the item's index value, because item numbering begins with 1 from the user's point of view. Any characters left on the echo area from a previous call are cleared away (lines 303-305). The cursor remains at the character position immediately to the right of the item number just displayed, to give the user a sense of continuity when entering a multiple digit item number.
The loop in lines 308-385 processes a keystroke. It runs only once, except in the special case when either the ESC key or an illegal digit is pressed (see below).
A keystroke is fetched via the getch function in line 310, and the digits flag is cleared if the key is not a decimal digit. Lines 315-354 then handle all the direct command mappings.
Lines 356-383 process the digit keys. Once a digit key is recognized, does the decimal value formed by the concatenation of this new key onto the decimal total of previous contiguous keystrokes yield a new item number within legal range for the current menu? The conditional test in lines 361-362 asks this question. If the answer is yes, then the integer pointed to by sel_val is set to the value of the new item number and get_cmd returns K_DIRECT to indicate a new item has been directly selected. If the answer to the above question is no, then the digit is still construed as legal if it alone represents a legal item number. Line 369 checks for that possibility. If true, then the echo area is cleared and the new item number is displayed there; again the selection value is assigned through the sel_val pointer and get_cmd returns the value K_DIRECT.
Any digit that fails both of the above criteria for legality generates a warning bell (line 382) and is otherwise ignored.
get_cmd interprets the ESC key as a special command meaning "clear the digit echo area." If this command were not available, aborting a direct item number entry sequence could get confusing because of the cumulative handling of digit strings. For example, consider a menu containing thirty items, and a user mistakenly presses the digit 1 when meaning to select item 2. If the next keystroke is the digit 2, item twelve would be selected. If 2 is pressed again, item 2 would be selected (because there is no item 122). The ESC key could have cleared the numeric selection after the initial digit 1 keystroke, and let the user press 2 to select item 2.
In my next installment of this series, we'll see how rmenu executes actions and how the machine-dependent portions of the program are encapsulated (might as well throw in an object-oriented buzzword; everybody else does!)