/* OpenDoors Online Software Programming Toolkit * (C) Copyright 1991 - 1999 by Brian Pirie. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * * File: ODPopUp.c * * Description: Implements od_popup_menu(), for displaying a menu in * a window, allowing the user to make a selection using * "hot keys" or by using arrow keys. * * Revisions: Date Ver Who Change * --------------------------------------------------------------- * Oct 13, 1994 6.00 BP New file header format. * Dec 09, 1994 6.00 BP Standardized coding style. * Jan 15, 1995 6.00 BP Free menu structure on menu destroy. * Feb 02, 1995 6.00 BP Added od_yield() call in for(;;) loop. * Aug 19, 1995 6.00 BP 32-bit portability. * Nov 11, 1995 6.00 BP Removed register keyword. * Nov 14, 1995 6.00 BP Change valid range of nLevel to 0-10. * Nov 16, 1995 6.00 BP Removed oddoor.h, added odcore.h. * Nov 17, 1995 6.00 BP Use new input queue mechanism. * Dec 12, 1995 6.00 BP Added entry, exit and kernel macros. * Dec 23, 1995 6.00 BP Restore original color on exit. * Dec 30, 1995 6.00 BP Added ODCALL for calling convention. * Jan 04, 1996 6.00 BP Use od_get_input(). * Jan 12, 1996 6.00 BP Claim exclusive use of arrow keys. * Jan 30, 1996 6.00 BP Replaced od_yield() with od_sleep(). * Jan 31, 1996 6.00 BP Added timeout for od_get_input(). * Jan 31, 1996 6.00 BP Add ODPopupCheckForKey() wait param. * Jan 31, 1996 6.00 BP Ignore left & right if !MENU_PULLDOWN. * Feb 13, 1996 6.00 BP Added od_get_input() flags parameter. * Feb 19, 1996 6.00 BP Changed version number to 6.00. * Mar 03, 1996 6.10 BP Begin version 6.10. * Aug 10, 2003 6.23 SH *nix support */ #define BUILDING_OPENDOORS #include #include #include #include "OpenDoor.h" #include "ODCore.h" #include "ODGen.h" #include "ODPlat.h" #include "ODKrnl.h" #include "ODStat.h" /* Configurable od_popup_menu() parameters. */ /* Maximum menu level. */ #define MENU_LEVELS 11 /* Maximum number of items in a menu. */ #define MAX_MENU_ITEMS 21 /* Maximum width of any menu item. */ #define MAX_ITEM_WIDTH 76 /* Other manifest constants. */ #define NO_COMMAND -10 /* Local data types. */ /* Information on an individual menu item. */ typedef struct { char szItemText[MAX_ITEM_WIDTH + 1]; BYTE btKeyIndex; } tMenuItem; /* Information on a popup menu level. */ typedef struct { tMenuItem *paMenuItems; BYTE btNumMenuItems; BYTE btWidth; BYTE btRight; BYTE btBottom; BYTE btCursor; BYTE btLeft; BYTE btTop; WORD wFlags; void *pWindow; } tMenuLevelInfo; /* Private variables. */ /* Array of information on each menu level. */ tMenuLevelInfo MenuLevelInfo[MENU_LEVELS]; /* Current menu settings. */ static BYTE btCorrectItem; static INT nCommand; static WORD wCurrentFlags; static BYTE btCurrentNumMenuItems; static INT nCurrentLevel; /* Private helper functions used by od_popup_menu(). */ static void ODPopupCheckForKey(BOOL bWaitForInput); static void ODPopupDisplayMenuItem(BYTE btLeft, BYTE btTop, tMenuItem *paMenuItems, BYTE btItemIndex, BOOL bHighlighted, BYTE btWidth, BOOL bPositionCursor); /* ---------------------------------------------------------------------------- * od_popup_menu() * * Displays a popup menu on the local and remote screens. * * Parameters: pszTitle - Text to show as the window title of the popup menu. * If no title is desired, this parameter should be set * to either "" or NULL. * * pszText - String which contains the menu definition. In the * menu definition string, individual menu items are * separated by a pipe ('|') character, and hotkeys are * proceeded by a carat ('^') character. * * nLeft - The 1-based column number of the upper right corner * of the menu. * * nTop - The 1-based row number of the upper right corner of * the menu. * * nLevel - Menu level, which must be a value between 0 and * MENU_LEVELS. * * uFlags - One or more flags, combined by the bitwise or (|) * operator. * * Return: POPUP_ERROR on error, POPUP_ESCAPE if user pressed the Escape * key, POPUP_LEFT if the user choose to move to the next menu to * the left, POPUP_RIGHT if the user choose to move to the next * menu to the right, or a postive value if the user choose an item * from the menu. In this case, the return value is the 1-based * index of the selected menu item. */ ODAPIDEF INT ODCALL od_popup_menu(char *pszTitle, char *pszText, INT nLeft, INT nTop, INT nLevel, WORD uFlags) { tMenuItem *paMenuItems = NULL; BYTE btCount; BYTE btWidth; BYTE btRight; BYTE btBottom; BYTE btCursor; BYTE btLeft; BYTE btTop; void *pWindow; BYTE btBetweenSize; BYTE btTitleSize; BYTE btRemaining; BYTE btLineCount; INT16 nOriginalAttrib; /* Log function entry if running in trace mode. */ TRACE(TRACE_API, "od_popup_menu()"); /* Initialize OpenDoors, if not already done. */ if(!bODInitialized) od_init(); OD_API_ENTRY(); /* Setup od_box_chars appropriately. */ if(od_control.od_box_chars[BOX_BOTTOM] == 0) { od_control.od_box_chars[BOX_BOTTOM] = od_control.od_box_chars[BOX_TOP]; } if(od_control.od_box_chars[BOX_RIGHT] == 0) { od_control.od_box_chars[BOX_RIGHT] = od_control.od_box_chars[BOX_LEFT]; } /* Store initial display color. */ nOriginalAttrib = od_control.od_cur_attrib; /* check level bounds */ if(nLevel < 0 || nLevel > MENU_LEVELS) { od_control.od_error = ERR_LIMIT; OD_API_EXIT(); return(POPUP_ERROR); } /* normalize level */ nCurrentLevel = nLevel; if(MenuLevelInfo[nLevel].pWindow == NULL) { btLeft = nLeft; btTop = nTop; wCurrentFlags = uFlags; if(pszText == NULL) { od_control.od_error = ERR_PARAMETER; OD_API_EXIT(); return(POPUP_ERROR); } if(paMenuItems == NULL) { if((paMenuItems = malloc(sizeof(tMenuItem) * MAX_MENU_ITEMS)) == NULL) { od_control.od_error = ERR_PARAMETER; OD_API_EXIT(); return(POPUP_ERROR); } } MenuLevelInfo[nLevel].paMenuItems = paMenuItems; btCurrentNumMenuItems = 0; btWidth = 0; btCount = 0; nCommand = NO_COMMAND; paMenuItems[0].btKeyIndex = 0; while(*pszText && btCurrentNumMenuItems < MAX_MENU_ITEMS) { switch(*pszText) { case '|': paMenuItems[btCurrentNumMenuItems++].szItemText[btCount] = '\0'; if(btCount > btWidth) btWidth = btCount; btCount = 0; paMenuItems[btCurrentNumMenuItems].btKeyIndex = 0; break; case '^': if(btCount < MAX_ITEM_WIDTH) { paMenuItems[btCurrentNumMenuItems].btKeyIndex = btCount; } break; default: if(btCount < MAX_ITEM_WIDTH) { paMenuItems[btCurrentNumMenuItems].szItemText[btCount++] = *pszText; } } ++pszText; } /* If we were in the middle of a menu item when we encountered the end */ /* of the string, then it should form an additional menu entry. This */ /* handles the case of a menu string to no terminating | for the last */ /* entry. */ if(btCount != 0) { /* null-terminate current menu entry string */ paMenuItems[btCurrentNumMenuItems++].szItemText[btCount] = '\0'; /* If this is the widest entry, update he menu width appropriately */ if(btCount > btWidth) btWidth = btCount; } /* If the menu description string does not contain any menu items */ if(btCurrentNumMenuItems == 0) { /* Return with parameter error */ od_control.od_error = ERR_PARAMETER; OD_API_EXIT(); return(POPUP_ERROR); } /* Adjust menu width to allow title to fit, if possible */ /* If a title string was passed, and that string is wider than widest */ /* menu entry ... */ if(pszTitle != NULL && strlen(pszTitle) + 2 > btWidth) { /* Then width of menu window should be large enough to allow up to */ /* the first 76 characters of the title to fit. */ btWidth = strlen(pszTitle) + 2 > MAX_ITEM_WIDTH ? MAX_ITEM_WIDTH : strlen(pszTitle) + 2; } /* Based on number and size of menu items, and width of title, */ /* determine the bottom, right and inside width of the menu. */ btBottom = btTop + btCurrentNumMenuItems + 1; btRight = btLeft + btWidth + 3; btBetweenSize = (btRight - btLeft) - 1; /* If neither ANSI nor AVATAR mode is available, return with an error */ if(!(od_control.user_ansi || od_control.user_avatar)) { od_control.od_error = ERR_NOGRAPHICS; OD_API_EXIT(); return(POPUP_ERROR); } /* If menu would "fall off" edge of screen, return with an error */ if(btLeft < 1 || btTop < 1 || btRight > OD_SCREEN_WIDTH || btBottom > OD_SCREEN_HEIGHT || btRight - btLeft < 2 || btBottom - btTop < 2) { od_control.od_error = ERR_PARAMETER; OD_API_EXIT(); return(POPUP_ERROR); } /* Allocate space to store window information. If unable to allocate */ /* enough space, return with an error. */ if((pWindow = malloc((btRight - btLeft + 1) * 2 + (btBottom - btTop + 1) * 160)) == NULL) { od_control.od_error = ERR_MEMORY; OD_API_EXIT(); return(POPUP_ERROR); } /* Store contents of screen where memu will be drawn in the temporary */ /* buffer. */ if(!od_gettext(btLeft, btTop, btRight, btBottom, pWindow)) { free(pWindow); pWindow = NULL; /* Note that od_error code has been set in od_gettext(). */ OD_API_EXIT(); return(POPUP_ERROR); } /* Determine number of characters of title to be displayed */ if(pszTitle == NULL) { btTitleSize = 0; } else { if((btTitleSize = strlen(pszTitle)) > (btBetweenSize - 4)) { btTitleSize = btBetweenSize - 4; } } od_set_cursor(btTop,btLeft); od_set_attrib(od_control.od_menu_border_col); od_putch(od_control.od_box_chars[BOX_UPPERLEFT]); if(btTitleSize == 0) { od_repeat(od_control.od_box_chars[BOX_TOP], btBetweenSize); } else { od_repeat(od_control.od_box_chars[BOX_TOP], btRemaining = ((btBetweenSize - btTitleSize - 2) / 2)); od_set_attrib(od_control.od_menu_title_col); od_putch(' '); od_disp(pszTitle,btTitleSize, TRUE); od_putch(' '); od_set_attrib(od_control.od_menu_border_col); od_repeat(od_control.od_box_chars[BOX_TOP], (BYTE)(btBetweenSize - btRemaining - btTitleSize - 2)); } od_putch(od_control.od_box_chars[BOX_UPPERRIGHT]); btLineCount = btTop + 1; btCorrectItem = 0; ODPopupCheckForKey(FALSE); btCursor = btCorrectItem; for(btCount = 0; btCount < btCurrentNumMenuItems && btLineCount < btBottom; ++btCount) { ODPopupCheckForKey(FALSE); if(nCommand != NO_COMMAND && !(wCurrentFlags & MENU_KEEP)) { goto exit_now; } od_set_cursor(btLineCount,btLeft); od_putch(od_control.od_box_chars[BOX_LEFT]); od_set_attrib(od_control.od_menu_text_col); if(btCount == btCursor) { ODPopupDisplayMenuItem(btLeft, btTop, paMenuItems, btCount, TRUE, btWidth, FALSE); } else { ODPopupDisplayMenuItem(btLeft, btTop, paMenuItems, btCount, FALSE, btWidth, FALSE); } od_set_attrib(od_control.od_menu_border_col); od_putch(od_control.od_box_chars[BOX_RIGHT]); ++btLineCount; } od_set_cursor(btBottom, btLeft); od_putch(od_control.od_box_chars[BOX_LOWERLEFT]); od_repeat(od_control.od_box_chars[BOX_BOTTOM], btBetweenSize); od_putch(od_control.od_box_chars[BOX_LOWERRIGHT]); od_set_cursor(btTop + 1, btLeft + 1); } else { paMenuItems = MenuLevelInfo[nLevel].paMenuItems; btCurrentNumMenuItems = MenuLevelInfo[nLevel].btNumMenuItems; btWidth = MenuLevelInfo[nLevel].btWidth; btRight = MenuLevelInfo[nLevel].btRight; btBottom = MenuLevelInfo[nLevel].btBottom; btLeft = MenuLevelInfo[nLevel].btLeft; btTop = MenuLevelInfo[nLevel].btTop; wCurrentFlags = MenuLevelInfo[nLevel].wFlags; pWindow = MenuLevelInfo[nLevel].pWindow; btCorrectItem = btCursor = MenuLevelInfo[nLevel].btCursor; nCommand = NO_COMMAND; if(uFlags & MENU_DESTROY) { nCommand = POPUP_ESCAPE; goto destroy; } /* Otherwise, position flashing hardware cursor appropriately */ od_set_cursor(btTop + btCursor + 1, btLeft + 1); } /* Claim exclusive use of arrow keys. */ ODStatStartArrowUse(); for(;;) { ODPopupCheckForKey(TRUE); if(btCorrectItem != btCursor) { ODPopupDisplayMenuItem(btLeft, btTop, paMenuItems, btCursor, FALSE, btWidth, TRUE); btCursor = btCorrectItem; ODWaitDrain(25); ODPopupCheckForKey(FALSE); ODPopupDisplayMenuItem(btLeft, btTop, paMenuItems, btCursor, TRUE, btWidth, TRUE); } if(nCommand != NO_COMMAND) { goto exit_now; } } exit_now: if((!(wCurrentFlags & MENU_KEEP)) || nCommand <= 0) { destroy: od_puttext(btLeft, btTop, btRight, btBottom, pWindow); free(pWindow); MenuLevelInfo[nLevel].pWindow = NULL; if(paMenuItems != NULL) { free(paMenuItems); MenuLevelInfo[nLevel].paMenuItems = NULL; } } else if(wCurrentFlags & MENU_KEEP) { MenuLevelInfo[nLevel].paMenuItems = paMenuItems; MenuLevelInfo[nLevel].btNumMenuItems = btCurrentNumMenuItems; MenuLevelInfo[nLevel].btWidth = btWidth; MenuLevelInfo[nLevel].btRight = btRight; MenuLevelInfo[nLevel].btBottom = btBottom; MenuLevelInfo[nLevel].btCursor = btCursor; MenuLevelInfo[nLevel].btLeft = btLeft; MenuLevelInfo[nLevel].btTop = btTop; MenuLevelInfo[nLevel].wFlags = wCurrentFlags; MenuLevelInfo[nLevel].pWindow = pWindow; } /* Restore original display color. */ od_set_attrib(nOriginalAttrib); /* Release exclusive use of arrow keys. */ ODStatEndArrowUse(); OD_API_EXIT(); return(nCommand); } /* ---------------------------------------------------------------------------- * ODPopupCheckForKey() *** PRIVATE FUNCTION *** * * Checks whether or not the user has pressed any key. If one or more keys * have been pressed, then these keystrokes are processed. This function * returns when no more keys are waiting in the inbound buffer, or when a key * has been pressed that requires immediate action (such as the [ENTER] key). * * Parameters: bWaitForInput - Indicates whether this function should return * immediately if no input is waiting (FALSE), or * wait for the next input even before returning * (TRUE). * * Return: void */ static void ODPopupCheckForKey(BOOL bWaitForInput) { BYTE btCount; tODInputEvent InputEvent; BOOL bDoneAnythingYet = FALSE; /* Loop, processing keys. If a command has been selected, stop looping */ /* immediately. If there are no more keys waiting, stop looping */ while(nCommand == NO_COMMAND) { CALL_KERNEL_IF_NEEDED(); if(!od_get_input(&InputEvent, bWaitForInput && !bDoneAnythingYet ? OD_NO_TIMEOUT : 0, GETIN_NORMAL)) { /* Return right away if no input event is waiting. */ return; } bDoneAnythingYet = TRUE; if(InputEvent.EventType == EVENT_EXTENDED_KEY) { switch(InputEvent.chKeyPress) { case OD_KEY_UP: up_arrow: if(btCorrectItem == 0) { btCorrectItem = btCurrentNumMenuItems - 1; } else { --btCorrectItem; } break; case OD_KEY_DOWN: down_arrow: if(++btCorrectItem >= btCurrentNumMenuItems) { btCorrectItem = 0; } break; case OD_KEY_LEFT: left_arrow: if(wCurrentFlags & MENU_PULLDOWN) { nCommand = POPUP_LEFT; return; } break; case OD_KEY_RIGHT: right_arrow: if(wCurrentFlags & MENU_PULLDOWN) { nCommand = POPUP_RIGHT; return; } break; } } else if(InputEvent.EventType == EVENT_CHARACTER) { if(InputEvent.chKeyPress == '\n' || InputEvent.chKeyPress == '\r') { nCommand = btCorrectItem + 1; return; } else if(InputEvent.chKeyPress == 27) { if(wCurrentFlags & MENU_ALLOW_CANCEL) { nCommand = POPUP_ESCAPE; return; } } else { /* Check whether key is a menu "hot key" */ for(btCount = 0; btCount < btCurrentNumMenuItems; ++btCount) { if(toupper(MenuLevelInfo[nCurrentLevel].paMenuItems[btCount] .szItemText[MenuLevelInfo[nCurrentLevel].paMenuItems[btCount] .btKeyIndex]) == toupper(InputEvent.chKeyPress)) { btCorrectItem = btCount; nCommand = btCorrectItem + 1; return; } } /* At this point, we know that key was not one of the "hot keys" */ /* Check for 4, 6, 8 and 2 keys as arrow keys. */ if(InputEvent.chKeyPress == '4') { goto left_arrow; } else if(InputEvent.chKeyPress == '6') { goto right_arrow; } else if(InputEvent.chKeyPress == '8') { goto up_arrow; } else if(InputEvent.chKeyPress == '2') { goto down_arrow; } } } } } /* ---------------------------------------------------------------------------- * ODPopupDisplayMenuItem() *** PRIVATE FUNCTION *** * * Displays an individual menu item. * * Parameters: btLeft - Column number where the menu item will be * displayed. * * btTop - Row number where the menu item will be * displayed. * * paMenuItems - Pointer to array of available menu items. * * btItemIndex - Index into paMenuItems of the menu item that * is to be displayed. * * bHighlighted - TRUE if the items is to be displayed as * highlighted, FALSE if it is to be displayed as * non-highlighted. * * btWidth - Width of the menu item, in characters. * * bPositionCursor - TRUE if the cursor needs to be positioned * prior to drawing the menu item, FALSE if the * cursor is already in the required position. * * Return: void */ static void ODPopupDisplayMenuItem(BYTE btLeft, BYTE btTop, tMenuItem *paMenuItems, BYTE btItemIndex, BOOL bHighlighted, BYTE btWidth, BOOL bPositionCursor) { BYTE btCount; char *pchItemText; BYTE btKeyPosition; BYTE btTextColor; BYTE btKeyColor; /* Check that parameters are reasonable when operating in debug mode. */ ASSERT(paMenuItems != NULL); ASSERT(btItemIndex < MAX_MENU_ITEMS); ASSERT(btWidth < OD_SCREEN_WIDTH); ++btLeft; ++btTop; btTextColor = bHighlighted ? od_control.od_menu_highlight_col : od_control.od_menu_text_col; btKeyColor = bHighlighted ? od_control.od_menu_highkey_col : od_control.od_menu_key_col; pchItemText = (char *)(paMenuItems[btItemIndex].szItemText); btKeyPosition = paMenuItems[btItemIndex].btKeyIndex; if(bPositionCursor) od_set_cursor(btTop + btItemIndex, btLeft); od_set_attrib(btTextColor); od_putch(' '); for(btCount = 0; btCount < btWidth && *pchItemText; ++btCount) { if(btCount == btKeyPosition) { od_set_attrib(btKeyColor); od_putch(*pchItemText++); od_set_attrib(btTextColor); } else { od_putch(*pchItemText++); } } od_repeat(' ', (BYTE)((btWidth - btCount) + 1)); if(bPositionCursor) od_set_cursor(btTop + btItemIndex, btLeft); }