menucon.t | documentation |
#charset "us-ascii" /* * TADS 3 Library - Menu System, console edition * * This implements the menusys user interface for the traditional * console-mode interpreters. */ #include "adv3.h" /* ------------------------------------------------------------------------ */ /* * Banner windows. For the console version, we display the menu * components in banner windows. */ /* * The very top banner of the menu, which holds its title and * instructions. */ topMenuBanner: BannerWindow ; /* * The actual menu contents banner window. This displays the list of * menu items to choose from. */ contentsMenuBanner: BannerWindow ; /* * The long topic banner. This takes over the screen when we're * displaying a long topic item. */ longTopicBanner: BannerWindow ; /* ------------------------------------------------------------------------ */ /* * Menu Item - user interface implementation for the console */ modify MenuItem /* * Call menu.display when you're ready to show the menu. This * should be called on the top-level menu; we run the entire menu * display process, and return when the user exits from the menu * tree. */ display() { local oldStr; local flags; /* make sure the main window is flushed before we get going */ flushOutput(); /* set up with the top menu banner in place of the status line */ removeStatusLine(); showTopMenuBanner(self); /* * display the menu using the same mode that the statusline * has decided to use */ switch (statusLine.statusDispMode) { case StatusModeApi: /* use a border, unless we're taking over the whole screen */ flags = (fullScreenMode ? 0 : BannerStyleBorder); /* * use a scrollbar if possible; keep the text scrolled into * view as we show it */ flags |= BannerStyleVScroll | BannerStyleAutoVScroll; /* banner API mode - show our banner window */ contentsMenuBanner.showBanner(nil, BannerLast, nil, BannerTypeText, BannerAlignTop, nil, nil, flags); /* make the banner window the default output stream */ oldStr = contentsMenuBanner.setOutputStream(); /* make sure we restore the default output stream when done */ try { /* display and run our menu in HTML mode */ showMenuHtml(self); } finally { /* restore the original default output stream */ outputManager.setOutputStream(oldStr); /* remove the menu banner */ contentsMenuBanner.removeBanner(); } break; case StatusModeTag: /* HTML <banner> tag mode - just show our HTML contents */ showMenuHtml(self); /* remove the banner for the menu display */ "<banner remove id=MenuTitle>"; break; case StatusModeText: /* display and run our menu in text mode */ showMenuText(self); break; } /* we're done, so remove the top menu banner */ removeTopMenuBanner(); } /* * Display the menu in plain text mode. This is used when the * interpreter only supports the old tads2-style text-mode * single-line status area. * * Returns true if we should return to the parent menu, nil if the * user selected QUIT to exit the menu system entirely. */ showMenuText(topMenu) { local i, selection, len, key = '', loc; /* remember the key list */ curKeyList = topMenu.keyList; /* bring our contents up to date, as needed */ updateContents(); /* keep going until the player exits this menu level */ do { /* * For text mode, print the title, then show the menu * options as a numbered list, then ask the player to make a * selection. */ /* get the number of items in the menu */ len = contents.length(); /* show the menu heading */ "\n<b><<heading>></b>\b"; /* show the contents as a numbered list */ for (i = 1; i <= len; i++) { /* leave room for two-digit numeric labels if needed */ if (len > 9 && i <= 10) "\ "; /* show the item's number and title */ "<<i>>.\ <<contents[i].title>>\n"; } /* show the main prompt */ gLibMessages.textMenuMainPrompt(topMenu.keyList); /* main input loop */ do { /* * Get a key, and convert any alphabetics to lower-case. * Do not allow real-time interruptions, as menus are * meta-game interactions. */ key = inputManager.getKey(nil, nil).toLower(); /* check for a command key */ loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil}); /* also check for a numeric selection */ selection = toInteger(key); } while ((selection < 1 || selection > len) && loc != M_QUIT && loc != M_PREV); /* * show the selection if it's an ordinary key (an ordinary * key is represented by a single character; if we have more * than one character, it's one of the '[xxx]' special key * representations) */ if (key.length() == 1) "<<key>>"; /* add a blank line */ "\b"; /* * If the selection is a number, then the player selected * that menu option. Call that submenu or topic's display * routine. If the routine returns nil, the player selected * QUIT, so we should quit as well. */ while (selection != 0 && selection <= contents.length()) { /* invoke the child menu */ loc = contents[selection].showMenuText(topMenu); /* * Check the result. If it's nil, it means QUIT; if it's * 'next', it means we're to proceed directly to our next * sub-menu. If the user didn't select QUIT, then * refresh our menu contents, as we'll be displaying our * menu again and its contents could have been affected * by the sub-menu invocation. */ switch(loc) { case M_QUIT: /* they want to quit - leave the submenu loop */ selection = 0; break; case M_UP: /* they want to go to the previous menu directly */ --selection; break; case M_DOWN: /* they want to go to the next menu directly */ ++selection; break; case M_PREV: /* * they want to show this menu again - update our * contents so that we account for any changes made * while running the submenu, then leave the submenu * loop */ updateContents(); selection = 0; /* * forget the 'prev' command - we don't want to back * up any further just yet, since the submenu just * wanted to get back to this point */ loc = nil; break; } } } while (loc != M_QUIT && loc != M_PREV); /* return the desired next action */ return loc; } /* * Show the menu using HTML. Return nil when the user selects QUIT * to exit the menu entirely. */ showMenuHtml(topMenu) { local len, selection = 1, loc; local refreshTitle = true; /* remember the key list */ curKeyList = topMenu.keyList; /* update the menu contents, as needed */ updateContents(); /* keep going until the user exits this menu level */ do { /* refresh our title in the instructions area if necessary */ if (refreshTitle) { refreshTopMenuBanner(topMenu); refreshTitle = nil; } /* get the number of items in the menu */ len = contents.length(); /* check whether we're in banner API or <banner> tag mode */ if (statusLine.statusDispMode == StatusModeApi) { /* banner API mode - clear our window */ contentsMenuBanner.clearWindow(); /* advise the interpreter of our best guess for our size */ if (fullScreenMode) contentsMenuBanner.setSize(100, BannerSizePercent, nil); else contentsMenuBanner.setSize(len + 1, BannerSizeAbsolute, true); /* set up our desired color scheme */ "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >"; } else { /* * <banner> tag mode - set up our tag. In full-screen * mode, set our height to 100% immediately; otherwise, * leave the height unspecified so that we'll use the * size of our contents. Use a border only if we're not * taking up the full screen. */ "<banner id=MenuBody align=top <<fullScreenMode ? 'height=100%' : 'border'>> ><body bgcolor=<<bgcolor>> text=<<fgcolor>> >"; } /* display our contents as a table */ "<table><tr><td width=<<indent>> > </td><td>"; for (local i = 1; i <= len; i++) { /* * To get the alignment right, we have to print '>' on * each and every line. However, we print it in the * background color to make it invisible everywhere but * in front of the current selection. */ if (selection != i) "<font color=<<bgcolor>> >></font>"; else ">"; /* make each selection a plain (i.e. unhilighted) HREF */ "<a plain href=<<i>> ><<contents[i].title>></a><br>"; } /* end the table */ "</td></tr></table>"; /* finish our display as appropriate */ if (statusLine.statusDispMode == StatusModeApi) { /* banner API - size the window to its contents */ if (!fullScreenMode) contentsMenuBanner.sizeToContents(); } else { /* <banner> tag - just close the tag */ "</banner>"; } /* main input loop */ do { local key, events; /* * Read an event - don't allow real-time interruptions, * since menus are meta-game interactions. Read an * event rather than just a keystroke, because we want * to let the user click on a menu item's HREF. */ events = inputManager.getEvent(nil, nil); /* check the event type */ switch (events[1]) { case InEvtHref: /* * the HREF's value is the selection number, or a * 'previous' command */ if (events[2] == 'previous') loc = M_PREV; else { selection = toInteger(events[2]); loc = M_SEL; } break; case InEvtKey: /* keystroke - convert any alphabetic to lower case */ key = events[2].toLower(); /* scan for a valid command key */ loc = topMenu.keyList.indexWhich( {x: x.indexOf(key) != nil}); break; } /* handle arrow keys */ if (loc == M_UP) { selection--; if (selection < 1) selection = len; } else if (loc == M_DOWN) { selection++; if (selection > len) selection = 1; } } while (loc == nil); /* if the player selected a sub-menu, invoke the selection */ while (loc == M_SEL && selection != 0 && selection <= contents.length()) { /* * Invoke the sub-menu, checking for a QUIT result. If * the user isn't quitting, we'll display our own menu * again; in this case, update it now, in case something * in the sub-menu changed our own contents. */ loc = contents[selection].showMenuHtml(topMenu); /* see what we have */ switch (loc) { case M_UP: /* they want to go directly to the previous menu */ loc = M_SEL; --selection; break; case M_DOWN: /* they want to go directly to the next menu */ loc = M_SEL; ++selection; break; case M_PREV: /* they want to return to this menu level */ loc = nil; /* update our contents */ updateContents(); /* make sure we refresh the title area */ refreshTitle = true; break; } } } while (loc != M_QUIT && loc != M_PREV); /* return the next status */ return loc; } /* * showTopMenuBanner creates the banner for the menu using the * banner API. The banner contains the title of the menu on the * left and the navigation keys on the right. */ showTopMenuBanner(topMenu) { /* do not show the top banner if we're in text mode */ if (statusLine.statusDispMode == StatusModeText) return; /* * Since the status line has already figured out the terp's * capabilities, piggyback off of what it learned. If we're * using banner API mode, show our banner window. */ if (statusLine.statusDispMode == StatusModeApi) { /* banner API mode - show our banner window */ topMenuBanner.showBanner(nil, BannerFirst, nil, BannerTypeText, BannerAlignTop, nil, nil, BannerStyleBorder | BannerStyleTabAlign); /* advise the terp that we need two lines */ topMenuBanner.setSize(2, BannerSizeAbsolute, true); } /* show our contents */ refreshTopMenuBanner(topMenu); } /* * Refresh the contents of the top bar with the instructions */ refreshTopMenuBanner(topMenu) { local oldStr; /* clear our old contents using the appropriate mode */ switch (statusLine.statusDispMode) { case StatusModeApi: /* clear the window */ topMenuBanner.clearWindow(); /* set the default output stream to our menu window */ oldStr = topMenuBanner.setOutputStream(); /* set our color scheme */ "<body bgcolor=<<topbarbg>> text=<<topbarfg>> >"; break; case StatusModeTag: /* start a new <banner> tag */ "<banner id=MenuTitle align=top><body bgcolor=<<topbarbg>> text=<<topbarfg>> >"; break; } /* show our heading */ say(heading); /* show our keyboard assignments */ gLibMessages.menuInstructions(topMenu.keyList, prevMenuLink); /* finish up according to our mode */ switch (statusLine.statusDispMode) { case StatusModeApi: /* banner API mode - restore the old output stream */ outputManager.setOutputStream(oldStr); /* size the window to the actual content size */ topMenuBanner.sizeToContents(); break; case StatusModeTag: /* close the <banner> tag */ "</banner>"; break; } } /* * Remove the top banner window */ removeTopMenuBanner() { /* remove the window according to the banner mode */ switch (statusLine.statusDispMode) { case StatusModeApi: /* banner API mode - remove the banner window */ topMenuBanner.removeBanner(); break; case StatusModeTag: /* banner tag mode - remove our banner tag */ "<banner remove id=MenuTitle>"; } } /* * Remove the status line banner prior to displaying the menu */ removeStatusLine() { local oldStr; /* remove the banner according to our banner display mode */ switch (statusLine.statusDispMode) { case StatusModeApi: /* * banner API mode - simply set the banner window to zero * size, which will effectively make it invisible */ statuslineBanner.setSize(0, BannerSizeAbsolute, nil); break; case StatusModeTag: /* <banner> tag mode - remove the statusline banner */ oldStr = outputManager.setOutputStream(statusTagOutputStream); "<banner remove id=StatusLine>"; outputManager.setOutputStream(oldStr); break; case StatusModeText: /* tads2-style statusline - there's no way to remove it */ break; } } ; /* ------------------------------------------------------------------------ */ /* * Menu topic item - console UI implementation */ modify MenuTopicItem /* * Display and run our menu in text mode. */ showMenuText(topMenu) { local i, len, loc; /* remember the key list */ curKeyList = topMenu.keyList; /* update our contents, as needed */ updateContents(); /* get the number of items in our list */ len = menuContents.length(); /* show our heading and instructions */ "\n<b><<heading>></b>"; gLibMessages.textMenuTopicPrompt(); /* * Show all of the items up to and including the last one we * displayed on any past invocation. Append "[#/#]" to each * item to show where we are in the overall list. */ for (i = 1 ; i <= lastDisplayed ; ++i) { /* display this item */ displaySubItem(i, i == lastDisplayed, '\b'); } /* main input loop */ for (;;) { local key; /* read a keystroke */ key = inputManager.getKey(nil, nil).toLower(); /* look it up in the key list */ loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil}); /* check to see if they want to quit the menu system */ if (loc == M_QUIT) return M_QUIT; /* * check to see if they want to return to the previous menu; * if we're out of items to show, return to the previous * menu on any other keystrok as well */ if (loc == M_PREV || self.lastDisplayed == len) return M_PREV; /* for any other keystroke, just show the next item */ lastDisplayed++; displaySubItem(lastDisplayed, lastDisplayed == len, '\b'); } } /* * Display and run our menu in HTML mode. */ showMenuHtml(topMenu) { local len; local topIdx; /* remember the key list */ curKeyList = topMenu.keyList; /* refresh the top instructions bar with our heading */ refreshTopMenuBanner(topMenu); /* update our contents, as needed */ updateContents(); /* get the number of items in our list */ len = menuContents.length(); /* * initially show the first item at the top of the window (we * might scroll the list later to show a later item at the top, * if we're limiting the number of items we can show at once) */ topIdx = 1; /* main interaction loop */ for (;;) { local lastIdx; /* redraw the window with the current top item */ lastIdx = redrawWinHtml(topIdx); /* process input */ for (;;) { local events; local loc; local key; /* read an event */ events = inputManager.getEvent(nil, nil); switch(events[1]) { case InEvtHref: /* check for a 'next' or 'previous' command */ switch(events[2]) { case 'next': /* we want to go to the next item */ loc = M_SEL; break; case 'previous': /* we want to go to the previous menu */ loc = M_PREV; break; default: /* ignore other hyperlinks */ loc = nil; } break; case InEvtKey: /* get the key, converting alphabetic to lower case */ key = events[2].toLower(); /* look up the keystroke in our key mappings */ loc = topMenu.keyList.indexWhich( {x: x.indexOf(key) != nil}); break; } /* * if they're quitting or returning to the previous * menu, we're done */ if (loc == M_QUIT || loc == M_PREV) return loc; /* advance to the next item if desired */ if (loc == M_SEL) { /* * if the last item we showed is the last item in * our entire list, then the normal selection keys * simply return to the previous menu */ if (lastIdx == len) return M_PREV; /* * If we haven't yet reached the last revealed item, * it means we're limited by the chunk size, so show * the next chunk. Otherwise, reveal the next item. */ if (lastIdx < lastDisplayed) { /* advance to the next chunk */ topIdx += chunkSize; } else { /* reveal the next item */ ++lastDisplayed; /* * if we're not in full-screen mode, and we've * already filled the window, scroll down a line * by advancing the index of the item at the top * of the window */ if (!fullScreenMode && lastIdx == topIdx + chunkSize - 1) ++topIdx; } /* done processing input */ break; } } } } /* * redraw the window in HTML mode, starting with the given item at * the top of the window */ redrawWinHtml(topIdx) { local len; local idx; /* get the number of items in our list */ len = menuContents.length(); /* check the banner mode (based on the statusline mode) */ if (statusLine.statusDispMode == StatusModeApi) { /* banner API mode - clear the window */ contentsMenuBanner.clearWindow(); /* * Advise the terp of our best guess at our size: assume one * line per item, and max out at either our actual number of * items or our maximum chunk size, whichever is lower. If * we're in full-screen mode, though, simply size to 100% of * the available space. */ if (fullScreenMode) contentsMenuBanner.setSize(100, BannerSizePercent, nil); else contentsMenuBanner.setSize(chunkSize < len ? chunkSize : len, BannerSizeAbsolute, true); /* set up our color scheme */ "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >"; } else { /* <banner> tag mode - open our tag */ "<banner id=MenuBody align=top <<fullScreenMode ? 'height=100%' : 'border'>> ><body bgcolor=<<bgcolor>> text=<<fgcolor>> >"; } /* start a table to show the items */ "<table><tr><td width=<<self.indent>> > </td><td>"; /* show the items */ for (idx = topIdx ; ; ++idx) { local isLast; /* * Note if this is the last item we're going to show just * now. It's the last item we're showing if it's the last * item in the list, or it's the 'lastDisplayed' item, or * we've filled out the chunk size. */ isLast = (idx == len || (!fullScreenMode && idx == topIdx + chunkSize - 1) || idx == lastDisplayed); /* display the next item */ displaySubItem(idx, isLast, '<br>'); /* if that was the last item, we're done */ if (isLast) break; } /* finish the table */ "</td></tr></table>"; /* finish the window */ switch(statusLine.statusDispMode) { case StatusModeApi: /* if we're not in full-screen mode, set the final size */ if (!fullScreenMode) contentsMenuBanner.sizeToContents(); break; case StatusModeTag: /* end the banner tag */ "</banner>"; break; } /* return the index of the last item displayed */ return idx; } /* * Display an item from our list. 'idx' is the index in our list of * the item to display. 'lastBeforeInput' indicates whether or not * this is the last item we're going to show before pausing for user * input. 'eol' gives the newline sequence to display at the end of * the line. */ displaySubItem(idx, lastBeforeInput, eol) { local item; /* get the item from our list */ item = menuContents[idx]; /* * show the item: if it's a simple string, just display it; * otherwise, assume it's an object, and call its getItemText * method to get its text (and possibly trigger any needed * side-effects) */ say(dataType(item) == TypeSString ? item : item.getItemText()); /* add the [n/m] indicator */ gLibMessages.menuTopicProgress(idx, menuContents.length()); /* * if this is the last item we're going to display before asking * for input, and it's not the last item in the list overall, * and we're in HTML mode, show a hyperlink for advancing to the * next item */ if (lastBeforeInput && idx != menuContents.length()) " <<aHrefAlt('next', nextMenuTopicLink, '')>>"; /* show the desired line-ending separator */ say(eol); /* if it's the last item, add the end-of-list marker */ if (idx == menuContents.length()) "<<menuTopicListEnd>>\n"; } ; /* ------------------------------------------------------------------------ */ /* * Long topic item */ modify MenuLongTopicItem /* display and run our menu in text mode */ showMenuText(topMenu) { local ret; /* remember the key list */ curKeyList = topMenu.keyList; /* take over the entire screen */ cls(); /* use the common handling */ ret = showMenuCommon(topMenu); /* we're done, so clear the screen again */ cls(); /* return the result from the common handler */ return ret; } /* display and run our menu in HTML mode */ showMenuHtml(topMenu) { local ret; local oldStr; /* remember the key list */ curKeyList = topMenu.keyList; /* update our contents, as needed */ updateContents(); /* hide the two menu system banners */ if (statusLine.statusDispMode == StatusModeApi) { local flags; /* * Our banner window might already be showing, because we * could be coming here directly from a prior chapter. If it * is, we don't need to show it again. If it isn't showing, * show it now. */ if (longTopicBanner.handle_ != nil) { /* simply clear our existing window */ longTopicBanner.clearWindow(); } else { /* hide the top menu banner */ topMenuBanner.setSize(0, BannerSizeAbsolute, nil); /* figure our flags */ flags = (fullScreenMode ? 0 : BannerStyleBorder) | BannerStyleVScroll | BannerStyleMoreMode | BannerStyleAutoVScroll; /* banner API mode - show the long-topic banner */ longTopicBanner.showBanner(contentsMenuBanner, BannerLast, nil, BannerTypeText, BannerAlignTop, 100, BannerSizePercent, flags); } /* use its output stream */ oldStr = longTopicBanner.setOutputStream(); /* set up our color scheme in the new banner */ "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >"; } else { /* * use the main game window output stream for printing this * text (we need to switch back to it explicitly, because * HTML-mode menus normally run in the context of the menu's * banner output stream) */ oldStr = outputManager.setOutputStream(mainOutputStream); /* we're using the main window, so clear out the game text */ cls(); } try { /* show our contents using the normal text display */ ret = showMenuCommon(topMenu); } finally { local chapter; /* restore the original output stream */ outputManager.setOutputStream(oldStr); /* * If we're going directly to the next or previous "chapter," * and the next menu is itself a long-topic item, don't clean * up the screen: simply leave it in place for the next item. * First, check for a next/previous chapter return code, and * get the menu object for the next/previous chapter. */ if (ret == M_DOWN) chapter = location.getNextMenu(self); else if (ret == M_UP) chapter = location.getPrevMenu(self); /* * if we have a next/previous chapter, and it's a long-topic * menu, we don't need cleanup; otherwise we do */ if (isChapterMenu && chapter != nil && chapter.ofKind(MenuLongTopicItem)) { /* we don't need any cleanup */ } else { /* clean up the window */ if (statusLine.statusDispMode == StatusModeApi) { /* API mode - remove our long-topic banner */ longTopicBanner.removeBanner(); } else { /* tag mode - we used the main game window, so clear it */ cls(); } /* restore the top menu banner window */ topMenu.showTopMenuBanner(topMenu); } } /* return the quit/continue indication */ return ret; } /* show our contents - common handler for text and HTML modes */ showMenuCommon(topMenu) { local evt, key, loc, nxt; /* update our contents, as needed */ updateContents(); /* show our heading, centered */ "<CENTER><b><<heading>></b></CENTER>\b"; /* show our contents */ "<<menuContents>>\b"; /* check to see if we should offer chapter navigation */ nxt = (isChapterMenu ? location.getNextMenu(self) : nil); /* if there's a next chapter, show how we can navigate to it */ if (nxt != nil) { /* show the navigation */ gLibMessages.menuNextChapter(topMenu.keyList, nxt.title, 'next', 'menu'); } else { /* no chaptering - just print the ending message */ "<<menuLongTopicEnd>>"; } /* wait for an event */ for (;;) { evt = inputManager.getEvent(nil, nil); switch(evt[1]) { case InEvtHref: /* check for a 'next' or 'prev' command */ if (evt[2] == 'next') return M_DOWN; else if (evt[2] == 'prev') return M_UP; else if (evt[2] == 'menu') return M_PREV; break; case InEvtKey: /* get the key */ key = evt[2].toLower(); /* * if we're in plain text mode, add a blank line after * the key input */ if (statusLine.statusDispMode == StatusModeText) "\b"; /* look up the command key */ loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil}); /* * if it's 'next', either proceed to the next menu or * return to the previous menu, depending on whether * we're in chapter mode or not */ if (loc == M_SEL) return (nxt == nil ? M_PREV : M_DOWN); /* if it's 'prev', return to the previous menu */ if (loc == M_PREV || loc == M_QUIT) return loc; /* ignore other keys */ break; } } } ;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3
Generated on 5/16/2013 from TADS version 3.1.3