cfad47cfa3/t3compiler/tads3/lib/adv3/menusys.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/*
4
 *   TADS 3 Library - Menu System
5
 *   
6
 *   Copyright 2003 by Stephen Granade
7
 *.  Modifications copyright 2003, 2006 Michael J. Roberts
8
 *   
9
 *   This module is designed to make it easy to add on-screen menus to a
10
 *   game.  Stephen Granade adapted this module from his TADS 2 menu
11
 *   system, and Mike Roberts made some minor cosmetic changes to integrate
12
 *   it with the main TADS 3 library.
13
 *   
14
 *   N.B. in plain-text mode (for interpreters without banner
15
 *   capabilities), a menu won't be fully usable if it exceeds 9 subitems:
16
 *   each item in a menu is numbered, and the user selects an item by
17
 *   entering its number; but we only accept a single digit as input, so
18
 *   only items 1 through 9 can be selected on any given menu.  Good
19
 *   usability design usually dictates that menus shouldn't be so large
20
 *   anyway, so most menus will naturally avoid this problem, but this is
21
 *   something to keep in mind.  
22
 */
23
24
#include "adv3.h"
25
26
27
/* 
28
 *   General instructions:
29
 *   
30
 *   Menus consist of MenuItems, MenuTopicItems, and MenuLongTopicItems.
31
 *   
32
 *   * MenuItems are the menu (and sub-menu) items that the player will
33
 *   select.  Their "title" attribute is what will be shown in the menu,
34
 *   and the "heading" attribute is shown as the heading while the menu
35
 *   itself is active; by default, the heading simply uses the title.
36
 *   
37
 *   * MenuTopicItems are for lists of topic strings that the player will
38
 *   be shown, like hints. "title" is what will be shown in the menu;
39
 *   "menuContents" is a list of either strings to be displayed, one at a
40
 *   time, or objects which must return a string via a "menuContents"
41
 *   method
42
 *   
43
 *   * MenuLongTopicItems are for longer discources. "title" is what will
44
 *   be shown in the menu; "menuContents" is either a string to be printed
45
 *   or a routine to be called.
46
 *   
47
 *   adv3.h contains templates for MenuItems, for your convenience.
48
 *   
49
 *   A simple example menu:
50
 *   
51
 *   FirstMenu: MenuItem 'Test menu';
52
 *.  + MenuItem 'Pets';
53
 *.  ++ MenuItem 'Chinchillas';
54
 *.  +++ MenuTopicItem 'About them'
55
 *.    menuContents = ['Furry', 'South American', 'Curious',
56
 *   'Note: Not a coat'];
57
 *.  +++ MenuTopicItem 'Benefits'
58
 *.    menuContents = ['Non-allergenic', 'Cute', 'Require little space'];
59
 *.  +++ MenuTopicItem 'Downsides'
60
 *.     menuContents = ['Require dust baths', 'Startle easily'];
61
 *.  ++ MenuItem 'Cats';
62
 *.  +++ MenuLongTopicItem 'Pure evil'
63
 *.     menuContents = 'Cats are, quite simply, pure evil. I would provide
64
 *.                     ample evidence were there room for it in this
65
 *.                     simple example.';
66
 *.  +++ MenuTopicItem 'Benefits'
67
 *.    menuContents = ['They, uh, well...', 'Okay, I can\'t think of any.'];
68
 */
69
70
71
/* 
72
 *   The very top banner of the menu, which holds its title and
73
 *   instructions.
74
 */
75
topMenuBanner: BannerWindow
76
;
77
78
/* 
79
 *   The actual menu contents banner window.  This displays the list of
80
 *   menu items to choose from.  
81
 */
82
contentsMenuBanner: BannerWindow
83
;
84
85
/*
86
 *   The long topic banner.  This takes over the screen when we're
87
 *   displaying a long topic item.  
88
 */
89
longTopicBanner: BannerWindow
90
;
91
92
/* ------------------------------------------------------------------------ */
93
/*
94
 *   A basic menu object.  This is an abstract base class that
95
 *   encapsulates some behavior common to different menu classes, and
96
 *   allows the use of the + syntax (like "+ MenuItem") to define
97
 *   containment.
98
 */
99
class MenuObject: object
100
    /* our contents list */
101
    contents = []
102
103
    /* 
104
     *   Since we're inheriting from object, but need to use the "+"
105
     *   syntax, we need to set up the contents appropriately
106
     */
107
    initializeLocation()
108
    {
109
        if (location != nil)
110
            location.addToContents(self);
111
    }
112
113
    /* add a menu item */
114
    addToContents(obj)
115
    {
116
        /* 
117
         *   If the menu has a nil menuOrder, and it inherits menuOrder
118
         *   from us, then it must be a dynamically-created object that
119
         *   doesn't provide a custom menuOrder.  Provide a suitable
120
         *   default of a value one higher than the highest menuOrder
121
         *   currently in our list, to ensure that the item always sorts
122
         *   after any items currently in the list. 
123
         */
124
        if (obj.menuOrder == nil && !overrides(obj, MenuObject, &menuOrder))
125
        {
126
            local maxVal;
127
            
128
            /* find the maximum current menuOrder value */
129
            maxVal = nil;
130
            foreach (local cur in contents)
131
            {
132
                /* 
133
                 *   if this one has a value, and it's the highest so far
134
                 *   (or the only one with a value we've found so far),
135
                 *   take it as the maximum so far 
136
                 */
137
                if (cur.menuOrder != nil
138
                    && (maxVal == nil || cur.menuOrder > maxVal))
139
                    maxVal = cur.menuOrder;
140
            }
141
142
            /* if we didn't find any values, use 0 as the arbitrary default */
143
            if (maxVal == nil)
144
                maxVal = 0;
145
146
            /* go one higher than the maximum of the existing items */
147
            obj.menuOrder = maxVal;
148
        }
149
150
        /* add the item to our contents list */
151
        contents += obj;
152
    }
153
154
    /*
155
     *   The menu order.  When we're about to show a list of menu items,
156
     *   we'll sort the list in ascending order of this property, then in
157
     *   ascending order of title.  By default, we set this order value to
158
     *   be equal to the menu item's sourceTextOrder. This makes the menu
159
     *   order default to the order of objects as defined in the source. If
160
     *   some other basis is desired, override topicOrder.  
161
     */
162
    menuOrder = (sourceTextOrder)
163
164
    /*
165
     *   Compare this menu object to another, for the purposes of sorting a
166
     *   list of menu items. Returns a positive number if this menu item
167
     *   sorts after the other one, a negative number if this menu item
168
     *   sorts before the other one, 0 if the relative order is arbitrary.
169
     *   
170
     *   By default, we'll sort by menuOrder if the menuOrder values are
171
     *   different, otherwise arbitrarily.  
172
     */
173
    compareForMenuSort(other)
174
    {
175
        /* 
176
         *   if one menuOrder value is nil, sort it earlier than the other;
177
         *   if they're both nil, they sort as equivalent 
178
         */
179
        if (menuOrder == nil && other.menuOrder == nil)
180
            return 0;
181
        else if (menuOrder == nil)
182
            return -1;
183
        else if (other.menuOrder == nil)
184
            return 1;
185
186
        /* return the difference of the sort order values */
187
        return menuOrder - other.menuOrder;
188
    }
189
190
    /* 
191
     *   Finish initializing our contents list.  This will be called on
192
     *   each MenuObject *after* we've called initializeLocation() on every
193
     *   object.  In other words, every menu will already have been added
194
     *   to its parent's contents; this can do anything else that's needed
195
     *   to initialize the contents list.  For example, some subclasses
196
     *   might want to sort their contents here, so that they list their
197
     *   menus in a defined order.  By default, we sort the menu items by
198
     *   menuOrder; subclasses can override this as needed.  
199
     */
200
    initializeContents()
201
    {
202
        /* sort our contents list in the object-defined sorting order */
203
        contents = contents.sort(
204
            SortAsc, {a, b: a.compareForMenuSort(b)});
205
    }
206
;
207
208
/* 
209
 *   This preinit object makes sure the MenuObjects all have their
210
 *   contents initialized properly.
211
 */
212
PreinitObject
213
    execute()
214
    {
215
        /* initialize each menu's location */
216
        forEachInstance(MenuObject, { menu: menu.initializeLocation() });
217
218
        /* do any extra work to initialize each menu's contents list */
219
        forEachInstance(MenuObject, { menu: menu.initializeContents() });
220
    }
221
;
222
223
/* 
224
 *   A MenuItem is a given item in the menu tree.  In general all you need
225
 *   to do to use menus is create a tree of MenuItems with titles.
226
 */
227
class MenuItem: MenuObject
228
    /* the name of the menu; this is listed in the parent menu */
229
    title = ''
230
231
    /* 
232
     *   the heading - this is shown when this menu is active; by default,
233
     *   we simply use the title 
234
     */
235
    heading = (title)
236
237
    /*
238
     *   Display properties.  These properties control the way the menu
239
     *   appears on the screen.  By default, a menu looks to its parent
240
     *   menu for display properties; this makes it easy to customize an
241
     *   entire menu tree, since changes in the top-level menu will cascade
242
     *   to all children that don't override these settings.  However, each
243
     *   menu can customize its own appearance by overriding these
244
     *   properties itself.
245
     *   
246
     *   'fgcolor' and 'bgcolor' are the foreground (text) and background
247
     *   colors, expressed as HTML color names (so '#nnnnnn' values can be
248
     *   used to specify RGB colors).
249
     *   
250
     *   'indent' is the number of pixels to indent the menu's contents
251
     *   from the left margin.  This is used only in HTML mode.
252
     *   
253
     *   'fullScreenMode' indicates whether the menu should take over the
254
     *   entire screen, or limit itself to the space it actually requires.
255
     *   Full screen mode makes the menu block out any game window text.
256
     *   Limited mode leaves the game window partially uncovered, but can
257
     *   be a bit jumpy, since the window changes size as the user
258
     *   navigates through different menus.  
259
     */
260
261
    /* foreground (text) and background colors, as HTML color names */
262
    fgcolor = (location != nil ? location.fgcolor : 'text')
263
    bgcolor = (location != nil ? location.bgcolor : 'bgcolor')
264
265
    /* 
266
     *   Foreground and background colors for the top instructions bar.
267
     *   By default, we use the color scheme of the parent menu, or the
268
     *   inverse of our main menu color scheme if we're the top menu. 
269
     */
270
    topbarfg = (location != nil ? location.topbarfg : 'statustext')
271
    topbarbg = (location != nil ? location.topbarbg : 'statusbg')
272
273
    /* number of spaces to indent the menu's contents */
274
    indent = (location != nil ? location.indent : '10')
275
    
276
    /* 
277
     *   full-screen mode: make our menu take up the whole screen (apart
278
     *   from the instructions bar, of course) 
279
     */
280
    fullScreenMode = (location != nil ? location.fullScreenMode : true)
281
    
282
    /* 
283
     *   The keys used to navigate the menus, in order:
284
     *   
285
     *   [quit, previous, up, down, select.]
286
     *   
287
     *   Since multiple keys can be used for the same navigation, the list
288
     *   is implemented as a List of Lists.  Keys must be given as
289
     *   lower-case in order to match input, since we convert all input
290
     *   keys to lower-case before matching them.
291
     *   
292
     *   In the sublist for each key, we use the first element as the key
293
     *   name we show in the instruction bar at the top of the screen.
294
     *   
295
     *   By default, we use our parent menu's key list, if we have a
296
     *   parent; if we have no parent, we use the standard keys from the
297
     *   library messages.  
298
     */
299
    keyList = (location != nil ? location.keyList : gLibMessages.menuKeyList)
300
301
    /* 
302
     *   the current key list - we'll set this on entry to the start of
303
     *   each showMenuXxx method, so that we keep track of the actual key
304
     *   list in use, as inherited from the top-level menu 
305
     */
306
    curKeyList = nil
307
308
    /*
309
     *   Title for the link to the previous menu, if any.  If the menu has
310
     *   a parent menu, we'll display this link next to the menu title in
311
     *   the top instructions/title bar.  If this is nil, we won't display
312
     *   a link at all.  Note that this can contain an HTML fragment; for
313
     *   example, you could use an <IMG> tag to display an icon here.  
314
     */
315
    prevMenuLink = (location != nil ? gLibMessages.prevMenuLink : nil)
316
317
    /* 
318
     *   Update our contents.  By default, we'll do nothing; subclasses
319
     *   can override this to manage dynamic menus if desired.  This is
320
     *   called just before the menu is displayed, each time it's
321
     *   displayed. 
322
     */
323
    updateContents() { }
324
325
    /* 
326
     *   Call menu.display when you're ready to show the menu.  This
327
     *   should be called on the top-level menu; we run the entire menu
328
     *   display process, and return when the user exits from the menu
329
     *   tree.  
330
     */
331
    display()
332
    {
333
        local oldStr;
334
        local flags;
335
336
        /* make sure the main window is flushed before we get going */
337
        flushOutput();
338
339
        /* set up with the top menu banner in place of the status line */
340
        removeStatusLine();
341
        showTopMenuBanner(self);
342
343
        /* 
344
         *   display the menu using the same mode that the statusline
345
         *   has decided to use 
346
         */
347
        switch (statusLine.statusDispMode)
348
        {
349
        case StatusModeApi:
350
            /* use a border, unless we're taking over the whole screen */
351
            flags = (fullScreenMode ? 0 : BannerStyleBorder);
352
353
            /* 
354
             *   use a scrollbar if possible; keep the text scrolled into
355
             *   view as we show it 
356
             */
357
            flags |= BannerStyleVScroll | BannerStyleAutoVScroll;
358
359
            /* banner API mode - show our banner window */
360
            contentsMenuBanner.showBanner(nil, BannerLast, nil,
361
                                          BannerTypeText, BannerAlignTop,
362
                                          nil, nil, flags);
363
364
            /* make the banner window the default output stream */
365
            oldStr = contentsMenuBanner.setOutputStream();
366
367
            /* make sure we restore the default output stream when done */
368
            try
369
            {
370
                /* display and run our menu in HTML mode */
371
                showMenuHtml(self);
372
            }
373
            finally
374
            {
375
                /* restore the original default output stream */
376
                outputManager.setOutputStream(oldStr);
377
378
                /* remove the menu banner */
379
                contentsMenuBanner.removeBanner();
380
            }
381
            break;
382
            
383
        case StatusModeTag:
384
            /* HTML <banner> tag mode - just show our HTML contents */
385
            showMenuHtml(self);
386
387
            /* remove the banner for the menu display */
388
            "<banner remove id=MenuTitle>";
389
            break;
390
            
391
        case StatusModeText:
392
            /* display and run our menu in text mode */
393
            showMenuText(self);
394
            break;
395
        }
396
397
        /* we're done, so remove the top menu banner */
398
        removeTopMenuBanner();
399
    }
400
    
401
    /*
402
     *   Display the menu in plain text mode.  This is used when the
403
     *   interpreter only supports the old tads2-style text-mode
404
     *   single-line status area.
405
     *   
406
     *   Returns true if we should return to the parent menu, nil if the
407
     *   user selected QUIT to exit the menu system entirely.  
408
     */
409
    showMenuText(topMenu)
410
    {
411
        local i, selection, len, key = '', loc;
412
413
        /* remember the key list */
414
        curKeyList = topMenu.keyList;
415
416
        /* bring our contents up to date, as needed */
417
        updateContents();
418
419
        /* keep going until the player exits this menu level */
420
        do
421
        {
422
            /* 
423
             *   For text mode, print the title, then show the menu
424
             *   options as a numbered list, then ask the player to make a
425
             *   selection.  
426
             */
427
            
428
            /* get the number of items in the menu */
429
            len = contents.length();              
430
431
            /* show the menu heading */
432
            "\n<b><<heading>></b>\b";
433
434
            /* show the contents as a numbered list */
435
            for (i = 1; i <= len; i++)
436
            {
437
                /* leave room for two-digit numeric labels if needed */
438
                if (len > 9 && i <= 10) "\ ";
439
440
                /* show the item's number and title */
441
                "<<i>>.\ <<contents[i].title>>\n";
442
            }
443
444
            /* show the main prompt */
445
            gLibMessages.textMenuMainPrompt(topMenu.keyList);
446
447
            /* main input loop */
448
            do
449
            {
450
                /* 
451
                 *   Get a key, and convert any alphabetics to lower-case.
452
                 *   Do not allow real-time interruptions, as menus are
453
                 *   meta-game interactions. 
454
                 */
455
                key = inputManager.getKey(nil, nil).toLower();
456
457
                /* check for a command key */
458
                loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil});
459
460
                /* also check for a numeric selection */
461
                selection = toInteger(key);
462
            } while ((selection < 1 || selection > len)
463
                     && loc != M_QUIT && loc != M_PREV);
464
465
            /* 
466
             *   show the selection if it's an ordinary key (an ordinary
467
             *   key is represented by a single character; if we have more
468
             *   than one character, it's one of the '[xxx]' special key
469
             *   representations) 
470
             */
471
            if (key.length() == 1)
472
                "<<key>>";
473
474
            /* add a blank line */
475
            "\b";
476
            
477
            /* 
478
             *   If the selection is a number, then the player selected
479
             *   that menu option.  Call that submenu or topic's display
480
             *   routine.  If the routine returns nil, the player selected
481
             *   QUIT, so we should quit as well. 
482
             */
483
            while (selection != 0 && selection <= contents.length())
484
            {
485
                /* invoke the child menu */
486
                loc = contents[selection].showMenuText(topMenu);
487
488
                /*   
489
                 *   Check the result.  If it's nil, it means QUIT; if it's
490
                 *   'next', it means we're to proceed directly to our next
491
                 *   sub-menu.  If the user didn't select QUIT, then
492
                 *   refresh our menu contents, as we'll be displaying our
493
                 *   menu again and its contents could have been affected
494
                 *   by the sub-menu invocation.  
495
                 */
496
                switch(loc)
497
                {
498
                case M_QUIT:
499
                    /* they want to quit - leave the submenu loop */
500
                    selection = 0;
501
                    break;
502
503
                case M_UP:
504
                    /* they want to go to the previous menu directly */
505
                    --selection;
506
                    break;
507
508
                case M_DOWN:
509
                    /* they want to go to the next menu directly */
510
                    ++selection;
511
                    break;
512
513
                case M_PREV:
514
                    /* 
515
                     *   they want to show this menu again - update our
516
                     *   contents so that we account for any changes made
517
                     *   while running the submenu, then leave the submenu
518
                     *   loop 
519
                     */
520
                    updateContents();
521
                    selection = 0;
522
523
                    /* 
524
                     *   forget the 'prev' command - we don't want to back
525
                     *   up any further just yet, since the submenu just
526
                     *   wanted to get back to this point 
527
                     */
528
                    loc = nil;
529
                    break;
530
                }
531
            }
532
        } while (loc != M_QUIT && loc != M_PREV);
533
534
        /* return the desired next action */
535
        return loc;
536
    }
537
538
    /*
539
     *   Show the menu using HTML.  Return nil when the user selects QUIT
540
     *   to exit the menu entirely.  
541
     */
542
    showMenuHtml(topMenu)
543
    {
544
        local len, selection = 1, loc;
545
        local refreshTitle = true;
546
        
547
        /* remember the key list */
548
        curKeyList = topMenu.keyList;
549
550
        /* update the menu contents, as needed */
551
        updateContents();
552
553
        /* keep going until the user exits this menu level */
554
        do
555
        {
556
            /* refresh our title in the instructions area if necessary */
557
            if (refreshTitle)
558
            {
559
                refreshTopMenuBanner(topMenu);
560
                refreshTitle = nil;
561
            }
562
563
            /* get the number of items in the menu */
564
            len = contents.length();
565
              
566
            /* check whether we're in banner API or <banner> tag mode */
567
            if (statusLine.statusDispMode == StatusModeApi)
568
            {
569
                /* banner API mode - clear our window */
570
                contentsMenuBanner.clearWindow();
571
572
                /* advise the interpreter of our best guess for our size */
573
                if (fullScreenMode)
574
                    contentsMenuBanner.setSize(100, BannerSizePercent, nil);
575
                else
576
                    contentsMenuBanner.setSize(len + 1, BannerSizeAbsolute,
577
                                               true);
578
579
                /* set up our desired color scheme */
580
                "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
581
            }
582
            else
583
            {
584
                /* 
585
                 *   <banner> tag mode - set up our tag.  In full-screen
586
                 *   mode, set our height to 100% immediately; otherwise,
587
                 *   leave the height unspecified so that we'll use the
588
                 *   size of our contents.  Use a border only if we're not
589
                 *   taking up the full screen. 
590
                 */
591
                "<banner id=MenuBody align=top
592
                <<fullScreenMode ? 'height=100%' : 'border'>>
593
                ><body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
594
            }
595
596
            /* display our contents as a table */
597
            "<table><tr><td width=<<indent>> > </td><td>";
598
            for (local i = 1; i <= len; i++)
599
            {
600
                /* 
601
                 *   To get the alignment right, we have to print '>' on
602
                 *   each and every line. However, we print it in the
603
                 *   background color to make it invisible everywhere but
604
                 *   in front of the current selection.
605
                 */
606
                if (selection != i)
607
                    "<font color=<<bgcolor>> >&gt;</font>";
608
                else
609
                    "&gt;";
610
                
611
                /* make each selection a plain (i.e. unhilighted) HREF */
612
                "<a plain href=<<i>> ><<contents[i].title>></a><br>";
613
            }
614
615
            /* end the table */
616
            "</td></tr></table>";
617
618
            /* finish our display as appropriate */
619
            if (statusLine.statusDispMode == StatusModeApi)
620
            {
621
                /* banner API - size the window to its contents */
622
                if (!fullScreenMode)
623
                    contentsMenuBanner.sizeToContents();
624
            }
625
            else
626
            {
627
                /* <banner> tag - just close the tag */
628
                "</banner>";
629
            }
630
631
            /* main input loop */
632
            do
633
            {
634
                local key, events;
635
636
                /* 
637
                 *   Read an event - don't allow real-time interruptions,
638
                 *   since menus are meta-game interactions.  Read an
639
                 *   event rather than just a keystroke, because we want
640
                 *   to let the user click on a menu item's HREF.  
641
                 */
642
                events = inputManager.getEvent(nil, nil);
643
644
                /* check the event type */
645
                switch (events[1])
646
                {
647
                case InEvtHref:
648
                    /* 
649
                     *   the HREF's value is the selection number, or a
650
                     *   'previous' command 
651
                     */
652
                    if (events[2] == 'previous')
653
                        loc = M_PREV;
654
                    else
655
                    {
656
                        selection = toInteger(events[2]);
657
                        loc = M_SEL;
658
                    }
659
                    break;
660
661
                case InEvtKey:
662
                    /* keystroke - convert any alphabetic to lower case */
663
                    key = events[2].toLower();
664
665
                    /* scan for a valid command key */
666
                    loc = topMenu.keyList.indexWhich(
667
                        {x: x.indexOf(key) != nil});
668
                    break;
669
                }
670
671
                /* handle arrow keys */
672
                if (loc == M_UP)
673
                {
674
                    selection--;
675
                    if (selection < 1)
676
                        selection = len;
677
                }
678
                else if (loc == M_DOWN)
679
                {
680
                    selection++;
681
                    if (selection > len)
682
                        selection = 1;
683
                }
684
            } while (loc == nil);
685
686
            /* if the player selected a sub-menu, invoke the selection */
687
            while (loc == M_SEL
688
                   && selection != 0
689
                   && selection <= contents.length())
690
            {
691
                /* 
692
                 *   Invoke the sub-menu, checking for a QUIT result.  If
693
                 *   the user isn't quitting, we'll display our own menu
694
                 *   again; in this case, update it now, in case something
695
                 *   in the sub-menu changed our own contents. 
696
                 */
697
                loc = contents[selection].showMenuHtml(topMenu);
698
                
699
                /* see what we have */
700
                switch (loc)
701
                {
702
                case M_UP:
703
                    /* they want to go directly to the previous menu */
704
                    loc = M_SEL;
705
                    --selection;
706
                    break;
707
708
                case M_DOWN:
709
                    /* they want to go directly to the next menu */
710
                    loc = M_SEL;
711
                    ++selection;
712
                    break;
713
714
                case M_PREV:
715
                    /* they want to return to this menu level */
716
                    loc = nil;
717
718
                    /* update our contents */
719
                    updateContents();
720
721
                    /* make sure we refresh the title area */
722
                    refreshTitle = true;
723
                    break;
724
                }
725
            }
726
        } while (loc != M_QUIT && loc != M_PREV);
727
728
        /* return the next status */
729
        return loc;
730
    }
731
732
    /* 
733
     *   showTopMenuBanner creates the banner for the menu using the
734
     *   banner API.  The banner contains the title of the menu on the
735
     *   left and the navigation keys on the right. 
736
     */
737
    showTopMenuBanner(topMenu)
738
    {
739
        /* do not show the top banner if we're in text mode */
740
        if (statusLine.statusDispMode == StatusModeText)
741
            return;
742
743
        /* 
744
         *   Since the status line has already figured out the terp's
745
         *   capabilities, piggyback off of what it learned.  If we're
746
         *   using banner API mode, show our banner window.  
747
         */
748
        if (statusLine.statusDispMode == StatusModeApi)
749
        {
750
            /* banner API mode - show our banner window */
751
            topMenuBanner.showBanner(nil, BannerFirst, nil, BannerTypeText,
752
                                     BannerAlignTop, nil, nil,
753
                                     BannerStyleBorder | BannerStyleTabAlign);
754
            
755
            /* advise the terp that we need two lines */
756
            topMenuBanner.setSize(2, BannerSizeAbsolute, true);
757
        }
758
759
        /* show our contents */
760
        refreshTopMenuBanner(topMenu);
761
    }
762
763
    /*
764
     *   Refresh the contents of the top bar with the instructions 
765
     */
766
    refreshTopMenuBanner(topMenu)
767
    {
768
        local oldStr;
769
770
        /* clear our old contents using the appropriate mode */
771
        switch (statusLine.statusDispMode)
772
        {
773
        case StatusModeApi:
774
            /* clear the window */
775
            topMenuBanner.clearWindow();
776
777
            /* set the default output stream to our menu window */
778
            oldStr = topMenuBanner.setOutputStream();
779
780
            /* set our color scheme */
781
            "<body bgcolor=<<topbarbg>> text=<<topbarfg>> >";
782
            break;
783
            
784
        case StatusModeTag:
785
            /* start a new <banner> tag */
786
            "<banner id=MenuTitle align=top><body bgcolor=<<topbarbg>>
787
            text=<<topbarfg>> >";
788
            break;
789
        }
790
791
        /* show our heading */
792
        say(heading);
793
794
        /* show our keyboard assignments */
795
        gLibMessages.menuInstructions(topMenu.keyList, prevMenuLink);
796
        
797
        /* finish up according to our mode */
798
        switch (statusLine.statusDispMode)
799
        {
800
        case StatusModeApi:
801
            /* banner API mode - restore the old output stream */
802
            outputManager.setOutputStream(oldStr);
803
            
804
            /* size the window to the actual content size */
805
            topMenuBanner.sizeToContents();
806
            break;
807
            
808
        case StatusModeTag:
809
            /* close the <banner> tag */
810
            "</banner>";
811
            break;
812
        }
813
    }
814
815
    /*
816
     *   Remove the top banner window
817
     */
818
    removeTopMenuBanner()
819
    {
820
        /* remove the window according to the banner mode */
821
        switch (statusLine.statusDispMode)
822
        {
823
        case StatusModeApi:
824
            /* banner API mode - remove the banner window */
825
            topMenuBanner.removeBanner();
826
            break;
827
828
        case StatusModeTag:
829
            /* banner tag mode - remove our banner tag */
830
            "<banner remove id=MenuTitle>";
831
        }
832
    }
833
    
834
    /*
835
     *   Remove the status line banner prior to displaying the menu
836
     */
837
    removeStatusLine()
838
    {
839
        local oldStr;
840
841
        /* remove the banner according to our banner display mode */
842
        switch (statusLine.statusDispMode)
843
        {
844
        case StatusModeApi:
845
            /* 
846
             *   banner API mode - simply set the banner window to zero
847
             *   size, which will effectively make it invisible 
848
             */
849
            statuslineBanner.setSize(0, BannerSizeAbsolute, nil);
850
            break;
851
852
        case StatusModeTag:
853
            /* <banner> tag mode - remove the statusline banner */
854
            oldStr = outputManager.setOutputStream(statusTagOutputStream);
855
            "<banner remove id=StatusLine>";
856
            outputManager.setOutputStream(oldStr);
857
            break;
858
859
        case StatusModeText:
860
            /* tads2-style statusline - there's no way to remove it */
861
            break;
862
        }
863
    }
864
865
    /*
866
     *   Get the next menu in our list following the given menu.  Returns
867
     *   nil if we don't find the given menu, or the given menu is the last
868
     *   menu. 
869
     */
870
    getNextMenu(menu)
871
    {
872
        /* find the menu in our contents list */
873
        local idx = contents.indexOf(menu);
874
875
        /* 
876
         *   if we found it, and it's not the last, return the menu at the
877
         *   next index; otherwise return nil 
878
         */
879
        return (idx != nil && idx < contents.length()
880
                ? contents[idx + 1] : nil);
881
    }
882
883
    /*
884
     *   Get the menu previous tot he given menu.  Returns nil if we don't
885
     *   find the given menu or the given menu is the first one. 
886
     */
887
    getPrevMenu(menu)
888
    {
889
        /* find the menu in our contents list */
890
        local idx = contents.indexOf(menu);
891
892
        /* 
893
         *   if we found it, and it's not the first, return the menu at the
894
         *   prior index; otherwise return nil 
895
         */
896
        return (idx != nil && idx > 1 ? contents[idx - 1] : nil);
897
    }
898
;
899
900
/*
901
 *   MenuTopicItem displays a series of entries successively.  This is
902
 *   intended to be used for displaying something like a list of hints for
903
 *   a topic.  Set menuContents to be a list of strings to be displayed.  
904
 */
905
class MenuTopicItem: MenuItem
906
    /* the name of this topic, as it appears in our parent menu */
907
    title = ''
908
909
    /* heading, displayed while we're showing this topic list */
910
    heading = (title)
911
912
    /* hyperlink text for showing the next menu */
913
    nextMenuTopicLink = (gLibMessages.nextMenuTopicLink)
914
915
    /* 
916
     *   A list of strings and/or MenuTopicSubItem items.  Each one of
917
     *   these is meant to be something like a single hint on our topic.
918
     *   We display these items one at a time when our menu item is
919
     *   selected.  
920
     */
921
    menuContents = []
922
923
    /* the index of the last item we displayed from our menuContents list */
924
    lastDisplayed = 1
925
926
    /* 
927
     *   The maximum number of our sub-items that we'll display at once.
928
     *   This is only used on interpreters with banner capabilities, and is
929
     *   ignored in full-screen mode.  
930
     */
931
    chunkSize = 6
932
933
    /* we'll display this after we've shown all of our items */
934
    menuTopicListEnd = (gLibMessages.menuTopicListEnd)
935
936
    /*
937
     *   Display and run our menu in text mode.
938
     */
939
    showMenuText(topMenu)
940
    {
941
        local i, len, loc;
942
        
943
        /* remember the key list */
944
        curKeyList = topMenu.keyList;
945
946
        /* update our contents, as needed */
947
        updateContents();
948
949
        /* get the number of items in our list */
950
        len = menuContents.length();
951
952
        /* show our heading and instructions */
953
        "\n<b><<heading>></b>";
954
        gLibMessages.textMenuTopicPrompt();
955
956
        /* 
957
         *   Show all of the items up to and including the last one we
958
         *   displayed on any past invocation.  Append "[#/#]" to each
959
         *   item to show where we are in the overall list. 
960
         */
961
        for (i = 1 ; i <= lastDisplayed ; ++i)
962
        {
963
            /* display this item */
964
            displaySubItem(i, i == lastDisplayed, '\b');
965
        }
966
967
        /* main input loop */
968
        for (;;)
969
        {
970
            local key;
971
            
972
            /* read a keystroke */
973
            key = inputManager.getKey(nil, nil).toLower();
974
975
            /* look it up in the key list */
976
            loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil});
977
978
            /* check to see if they want to quit the menu system */
979
            if (loc == M_QUIT)
980
                return M_QUIT;
981
982
            /* 
983
             *   check to see if they want to return to the previous menu;
984
             *   if we're out of items to show, return to the previous
985
             *   menu on any other keystrok as well 
986
             */
987
            if (loc == M_PREV || self.lastDisplayed == len)
988
                 return M_PREV;
989
990
            /* for any other keystroke, just show the next item */
991
            lastDisplayed++;
992
            displaySubItem(lastDisplayed, lastDisplayed == len, '\b');
993
        }
994
    }
995
996
    /*
997
     *   Display and run our menu in HTML mode.
998
     */
999
    showMenuHtml(topMenu)
1000
    {
1001
        local len;
1002
        local topIdx;
1003
        
1004
        /* remember the key list */
1005
        curKeyList = topMenu.keyList;
1006
1007
        /* refresh the top instructions bar with our heading */
1008
        refreshTopMenuBanner(topMenu);
1009
1010
        /* update our contents, as needed */
1011
        updateContents();
1012
1013
        /* get the number of items in our list */
1014
        len = menuContents.length();
1015
1016
        /* 
1017
         *   initially show the first item at the top of the window (we
1018
         *   might scroll the list later to show a later item at the top,
1019
         *   if we're limiting the number of items we can show at once) 
1020
         */
1021
        topIdx = 1;
1022
1023
        /* main interaction loop */
1024
        for (;;)
1025
        {
1026
            local lastIdx;
1027
1028
            /* redraw the window with the current top item */
1029
            lastIdx = redrawWinHtml(topIdx);
1030
1031
            /* process input */
1032
            for (;;)
1033
            {
1034
                local events;
1035
                local loc;
1036
                local key;
1037
                
1038
                /* read an event */
1039
                events = inputManager.getEvent(nil, nil);
1040
                switch(events[1])
1041
                {
1042
                case InEvtHref:
1043
                    /* check for a 'next' or 'previous' command */
1044
                    switch(events[2])
1045
                    {
1046
                    case 'next':
1047
                        /* we want to go to the next item */
1048
                        loc = M_SEL;
1049
                        break;
1050
                        
1051
                    case 'previous':
1052
                        /* we want to go to the previous menu */
1053
                        loc = M_PREV;
1054
                        break;
1055
1056
                    default:
1057
                        /* ignore other hyperlinks */
1058
                        loc = nil;
1059
                    }
1060
                    break;
1061
1062
                case InEvtKey:
1063
                    /* get the key, converting alphabetic to lower case */
1064
                    key = events[2].toLower();
1065
1066
                    /* look up the keystroke in our key mappings */
1067
                    loc = topMenu.keyList.indexWhich(
1068
                        {x: x.indexOf(key) != nil});
1069
                    break;
1070
                }
1071
1072
                /* 
1073
                 *   if they're quitting or returning to the previous
1074
                 *   menu, we're done 
1075
                 */
1076
                if (loc == M_QUIT || loc == M_PREV)
1077
                    return loc;
1078
                
1079
                /* advance to the next item if desired */
1080
                if (loc == M_SEL)
1081
                {
1082
                    /* 
1083
                     *   if the last item we showed is the last item in
1084
                     *   our entire list, then the normal selection keys
1085
                     *   simply return to the previous menu 
1086
                     */
1087
                    if (lastIdx == len)
1088
                        return M_PREV;
1089
1090
                    /* 
1091
                     *   If we haven't yet reached the last revealed item,
1092
                     *   it means we're limited by the chunk size, so show
1093
                     *   the next chunk.  Otherwise, reveal the next item.
1094
                     */
1095
                    if (lastIdx < lastDisplayed)
1096
                    {
1097
                        /* advance to the next chunk */
1098
                        topIdx += chunkSize;
1099
                    }
1100
                    else
1101
                    {
1102
                        /* reveal the next item */
1103
                        ++lastDisplayed;
1104
1105
                        /* 
1106
                         *   if we're not in full-screen mode, and we've
1107
                         *   already filled the window, scroll down a line
1108
                         *   by advancing the index of the item at the top
1109
                         *   of the window 
1110
                         */
1111
                        if (!fullScreenMode
1112
                            && lastIdx == topIdx + chunkSize - 1)
1113
                            ++topIdx;
1114
                    }
1115
1116
                    /* done processing input */
1117
                    break;
1118
                }
1119
            }
1120
        }
1121
    }
1122
1123
    /*
1124
     *   redraw the window in HTML mode, starting with the given item at
1125
     *   the top of the window 
1126
     */
1127
    redrawWinHtml(topIdx)
1128
    {
1129
        local len;
1130
        local idx;
1131
        
1132
        /* get the number of items in our list */
1133
        len = menuContents.length();
1134
1135
        /* check the banner mode (based on the statusline mode) */
1136
        if (statusLine.statusDispMode == StatusModeApi)
1137
        {
1138
            /* banner API mode - clear the window */
1139
            contentsMenuBanner.clearWindow();
1140
            
1141
            /* 
1142
             *   Advise the terp of our best guess at our size: assume one
1143
             *   line per item, and max out at either our actual number of
1144
             *   items or our maximum chunk size, whichever is lower.  If
1145
             *   we're in full-screen mode, though, simply size to 100% of
1146
             *   the available space.  
1147
             */
1148
            if (fullScreenMode)
1149
                contentsMenuBanner.setSize(100, BannerSizePercent, nil);
1150
            else
1151
                contentsMenuBanner.setSize(chunkSize < len ? chunkSize : len,
1152
                                           BannerSizeAbsolute, true);
1153
1154
            /* set up our color scheme */
1155
            "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
1156
        }
1157
        else
1158
        {
1159
            /* <banner> tag mode - open our tag */
1160
            "<banner id=MenuBody align=top
1161
            <<fullScreenMode ? 'height=100%' : 'border'>>
1162
            ><body bgcolor=<<bgcolor>> text=<<fgcolor>>  >";
1163
        }
1164
1165
        /* start a table to show the items */
1166
        "<table><tr><td width=<<self.indent>> > </td><td>";
1167
1168
        /* show the items */
1169
        for (idx = topIdx ; ; ++idx)
1170
        {
1171
            local isLast;
1172
1173
            /* 
1174
             *   Note if this is the last item we're going to show just
1175
             *   now.  It's the last item we're showing if it's the last
1176
             *   item in the list, or it's the 'lastDisplayed' item, or
1177
             *   we've filled out the chunk size.  
1178
             */
1179
            isLast = (idx == len
1180
                      || (!fullScreenMode && idx == topIdx + chunkSize - 1)
1181
                      || idx == lastDisplayed);
1182
            
1183
            /* display the next item */
1184
            displaySubItem(idx, isLast, '<br>');
1185
1186
            /* if that was the last item, we're done */
1187
            if (isLast)
1188
                break;
1189
        }
1190
1191
        /* finish the table */
1192
        "</td></tr></table>";
1193
1194
        /* finish the window */
1195
        switch(statusLine.statusDispMode)
1196
        {
1197
        case StatusModeApi:
1198
            /* if we're not in full-screen mode, set the final size */
1199
            if (!fullScreenMode)
1200
                contentsMenuBanner.sizeToContents();
1201
            break;
1202
1203
        case StatusModeTag:
1204
            /* end the banner tag */
1205
            "</banner>";
1206
            break;
1207
        }
1208
1209
        /* return the index of the last item displayed */
1210
        return idx;
1211
    }
1212
1213
    /* 
1214
     *   Display an item from our list.  'idx' is the index in our list of
1215
     *   the item to display.  'lastBeforeInput' indicates whether or not
1216
     *   this is the last item we're going to show before pausing for user
1217
     *   input.  'eol' gives the newline sequence to display at the end of
1218
     *   the line.  
1219
     */
1220
    displaySubItem(idx, lastBeforeInput, eol)
1221
    {
1222
        local item;
1223
1224
        /* get the item from our list */
1225
        item = menuContents[idx];
1226
1227
        /* 
1228
         *   show the item: if it's a simple string, just display it;
1229
         *   otherwise, assume it's an object, and call its getItemText
1230
         *   method to get its text (and possibly trigger any needed
1231
         *   side-effects) 
1232
         */
1233
        say(dataType(item) == TypeSString ? item : item.getItemText());
1234
1235
        /* add the [n/m] indicator */
1236
        gLibMessages.menuTopicProgress(idx, menuContents.length());
1237
1238
        /* 
1239
         *   if this is the last item we're going to display before asking
1240
         *   for input, and it's not the last item in the list overall,
1241
         *   and we're in HTML mode, show a hyperlink for advancing to the
1242
         *   next item 
1243
         */
1244
        if (lastBeforeInput && idx != menuContents.length())
1245
            "&emsp;<<aHrefAlt('next', nextMenuTopicLink, '')>>";
1246
1247
        /* show the desired line-ending separator */
1248
        say(eol);
1249
1250
        /* if it's the last item, add the end-of-list marker */
1251
        if (idx == menuContents.length())
1252
            "<<menuTopicListEnd>>\n";
1253
    }
1254
;
1255
1256
/*
1257
 *   A menu topic sub-item can be used to represent an item in a
1258
 *   MenuTopicItem's list of display items.  This can be useful when
1259
 *   displaying a topic must trigger a side-effect.  
1260
 */
1261
class MenuTopicSubItem: object
1262
    /*
1263
     *   Get the item's text.  By default, we just return an empty string.
1264
     *   This should be overridden to return the appropriate text, and can
1265
     *   also trigger any desired side-effects.  
1266
     */
1267
    getItemText() { return ''; }
1268
;
1269
1270
/* 
1271
 *   Long Topic Items are used to print out big long gobs of text on a
1272
 *   subject.  Use it for printing long treatises on your design
1273
 *   philosophy and the like.  
1274
 */
1275
class MenuLongTopicItem: MenuItem
1276
    /* the title of the menu, shown in parent menus */
1277
    title = ''
1278
1279
    /* the heading, shown while we're displaying our contents */
1280
    heading = (title)
1281
1282
    /* either a string to be displayed, or a routine */
1283
    menuContents = ''
1284
1285
    /* 
1286
     *   Flag - this is a "chapter" in a list of chapters.  If this is set
1287
     *   to true, then we'll offer the options to proceed directly to the
1288
     *   next and previous chapters.  If this is nil, we'll simply wait for
1289
     *   acknowledgment and return to the parent menu. 
1290
     */
1291
    isChapterMenu = nil
1292
1293
    /* the message we display at the end of our text */
1294
    menuLongTopicEnd = (gLibMessages.menuLongTopicEnd)
1295
1296
    /* display and run our menu in text mode */
1297
    showMenuText(topMenu)
1298
    {
1299
        local ret;
1300
        
1301
        /* remember the key list */
1302
        curKeyList = topMenu.keyList;
1303
1304
        /* take over the entire screen */
1305
        cls();
1306
1307
        /* use the common handling */
1308
        ret = showMenuCommon(topMenu);
1309
1310
        /* we're done, so clear the screen again */
1311
        cls();
1312
1313
        /* return the result from the common handler */
1314
        return ret;
1315
    }
1316
1317
    /* display and run our menu in HTML mode */
1318
    showMenuHtml(topMenu)
1319
    {
1320
        local ret;
1321
        local oldStr;
1322
1323
        /* remember the key list */
1324
        curKeyList = topMenu.keyList;
1325
1326
        /* update our contents, as needed */
1327
        updateContents();
1328
1329
        /* hide the two menu system banners */
1330
        if (statusLine.statusDispMode == StatusModeApi)
1331
        {
1332
            local flags;
1333
1334
            /* 
1335
             *   Our banner window might already be showing, because we
1336
             *   could be coming here directly from a prior chapter.  If it
1337
             *   is, we don't need to show it again.  If it isn't showing,
1338
             *   show it now.  
1339
             */
1340
            if (longTopicBanner.handle_ != nil)
1341
            {
1342
                /* simply clear our existing window */
1343
                longTopicBanner.clearWindow();
1344
            }
1345
            else
1346
            {
1347
                /* hide the top menu banner */
1348
                topMenuBanner.setSize(0, BannerSizeAbsolute, nil);
1349
1350
                /* figure our flags */
1351
                flags = (fullScreenMode ? 0 : BannerStyleBorder)
1352
                    | BannerStyleVScroll
1353
                    | BannerStyleMoreMode
1354
                    | BannerStyleAutoVScroll;
1355
            
1356
                /* banner API mode - show the long-topic banner */
1357
                longTopicBanner.showBanner(contentsMenuBanner, BannerLast,
1358
                                           nil, BannerTypeText,
1359
                                           BannerAlignTop,
1360
                                           100, BannerSizePercent, flags);
1361
            }
1362
1363
            /* use its output stream */
1364
            oldStr = longTopicBanner.setOutputStream();
1365
                
1366
            /* set up our color scheme in the new banner */
1367
            "<body bgcolor=<<bgcolor>> text=<<fgcolor>> >";
1368
        }
1369
        else
1370
        {
1371
            /* 
1372
             *   use the main game window output stream for printing this
1373
             *   text (we need to switch back to it explicitly, because
1374
             *   HTML-mode menus normally run in the context of the menu's
1375
             *   banner output stream) 
1376
             */
1377
            oldStr = outputManager.setOutputStream(mainOutputStream);
1378
1379
            /* we're using the main window, so clear out the game text */
1380
            cls();
1381
        }
1382
1383
        try
1384
        {
1385
            /* show our contents using the normal text display */
1386
            ret = showMenuCommon(topMenu);
1387
        }
1388
        finally
1389
        {
1390
            local chapter;
1391
            
1392
            /* restore the original output stream */
1393
            outputManager.setOutputStream(oldStr);
1394
1395
            /*
1396
             *   If we're going directly to the next or previous "chapter,"
1397
             *   and the next menu is itself a long-topic item, don't clean
1398
             *   up the screen: simply leave it in place for the next item.
1399
             *   First, check for a next/previous chapter return code, and
1400
             *   get the menu object for the next/previous chapter.  
1401
             */
1402
            if (ret == M_DOWN)
1403
                chapter = location.getNextMenu(self);
1404
            else if (ret == M_UP)
1405
                chapter = location.getPrevMenu(self);
1406
1407
            /* 
1408
             *   if we have a next/previous chapter, and it's a long-topic
1409
             *   menu, we don't need cleanup; otherwise we do 
1410
             */
1411
            if (isChapterMenu
1412
                && chapter != nil && chapter.ofKind(MenuLongTopicItem))
1413
            {
1414
                /* we don't need any cleanup */
1415
            }
1416
            else
1417
            {
1418
                /* clean up the window */
1419
                if (statusLine.statusDispMode == StatusModeApi)
1420
                {
1421
                    /* API mode - remove our long-topic banner */
1422
                    longTopicBanner.removeBanner();
1423
                }
1424
                else
1425
                {
1426
                    /* tag mode - we used the main game window, so clear it */
1427
                    cls();
1428
                }
1429
1430
                /* restore the top menu banner window */
1431
                topMenu.showTopMenuBanner(topMenu);
1432
            }
1433
        }
1434
1435
        /* return the quit/continue indication */
1436
        return ret;
1437
    }
1438
1439
    /* show our contents - common handler for text and HTML modes */
1440
    showMenuCommon(topMenu)
1441
    {
1442
        local evt, key, loc, nxt;
1443
1444
        /* update our contents, as needed */
1445
        updateContents();
1446
1447
        /* show our heading, centered */
1448
        "<CENTER><b><<heading>></b></CENTER>\b";
1449
1450
        /* show our contents */
1451
        "<<menuContents>>\b";
1452
1453
        /* check to see if we should offer chapter navigation */
1454
        nxt = (isChapterMenu ? location.getNextMenu(self) : nil);
1455
1456
        /* if there's a next chapter, show how we can navigate to it */
1457
        if (nxt != nil)
1458
        {
1459
            /* show the navigation */
1460
            gLibMessages.menuNextChapter(topMenu.keyList, nxt.title,
1461
                                         'next', 'menu');
1462
        }
1463
        else
1464
        {
1465
            /* no chaptering - just print the ending message */
1466
            "<<menuLongTopicEnd>>";
1467
        }
1468
1469
        /* wait for an event */
1470
        for (;;)
1471
        {
1472
            evt = inputManager.getEvent(nil, nil);
1473
            switch(evt[1])
1474
            {
1475
            case InEvtHref:
1476
                /* check for a 'next' or 'prev' command */
1477
                if (evt[2] == 'next')
1478
                    return M_DOWN;
1479
                else if (evt[2] == 'prev')
1480
                    return M_UP;
1481
                else if (evt[2] == 'menu')
1482
                    return M_PREV;
1483
                break;
1484
1485
            case InEvtKey:
1486
                /* get the key */
1487
                key = evt[2].toLower();
1488
1489
                /* 
1490
                 *   if we're in plain text mode, add a blank line after
1491
                 *   the key input 
1492
                 */
1493
                if (statusLine.statusDispMode == StatusModeText)
1494
                    "\b";
1495
1496
                /* look up the command key */
1497
                loc = topMenu.keyList.indexWhich({x: x.indexOf(key) != nil});
1498
1499
                /* 
1500
                 *   if it's 'next', either proceed to the next menu or
1501
                 *   return to the previous menu, depending on whether
1502
                 *   we're in chapter mode or not 
1503
                 */
1504
                if (loc == M_SEL)
1505
                    return (nxt == nil ? M_PREV : M_DOWN);
1506
1507
                /* if it's 'prev', return to the previous menu */
1508
                if (loc == M_PREV || loc == M_QUIT)
1509
                    return loc;
1510
1511
                /* ignore other keys */
1512
                break;
1513
            }
1514
        }
1515
    }
1516
;