# 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 "advlite.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. The MenuItem version of prop shouldn't be called with an argument, but it's
         *   possible that a caller intended the Object version of display, so we provide an optional
         *   parameter slot to alllow for this.
         *
         */
        display(prop?)
        {
            /*
             *   If display is called with a parameter, assume it's intended to be a call to Object's
             *   version of prop.
             */
            if(prop)
            {
                inherited BaseObject(prop);
                return;
            }

            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).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>> >&gt;</font>";
                    else
                        "&gt;";

                    /* 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);

                    /* 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).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);
                    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())
                "&emsp;<<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);
                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;
                }
            }
        }
    ;

Adv3Lite Library Reference Manual
Generated on 13/06/2025 from adv3Lite version 2.2.1