| | 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>> >></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 | 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 | " <<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 | ; |