1#charset "us-ascii" 2 3/* 4 * TADS 3 Library - Menu System 5 * 6 * Copyright 2003 by Stephen Granade 7 *. Modifications copyright 2003 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 */ 75topMenuBanner: BannerWindow 76; 77 78/* 79 * The actual menu contents banner window. This displays the list of 80 * menu items to choose from. 81 */ 82contentsMenuBanner: BannerWindow 83; 84 85/* 86 * The long topic banner. This takes over the screen when we're 87 * displaying a long topic item. 88 */ 89longTopicBanner: 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 */ 99class 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 */ 212PreinitObject 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 */ 227class 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 : libMessages.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 ? libMessages.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 libMessages.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>> >></font>"; 608 else 609 ">"; 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 libMessages.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 */ 905class 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 = (libMessages.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 = (libMessages.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 libMessages.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 libMessages.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 " <<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 */ 1261class 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 */ 1275class 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 = (libMessages.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 libMessages.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; 1517