forked from EricEve/adv3lite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
menusys.t
494 lines (439 loc) · 18 KB
/
menusys.t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
#charset "us-ascii"
/*
* TADS 3 Library - Menu System
*
* Copyright 2003 by Stephen Granade
*. Modifications copyright 2003, 2010 Michael J. Roberts
*
* This module is designed to make it easy to add on-screen menu trees to
* a game. Note that we're not using the term "menu" in its modern GUI
* sense of a compact, mouse-driven pop-up list. The style of menu we
* implement is more like the kind you'd find in old character-mode
* terminal programs, where a list of text items takes over the main
* window contents.
*
* Note that in plain-text mode (for interpreters without banner
* capabilities), a menu won't be fully usable if it exceeds nine
* subitems: each item in a menu is numbered, and the user selects an
* item by entering its number; but we only accept a single digit as
* input, so only items 1 through 9 can be selected on any given menu.
* In practice you probably wouldn't want to create larger menus anyway,
* for usability reasons, but this is something to keep in mind. If you
* need more items, you can group some of them into a submenu.
*
* The user interface for the menu system is implemented in menucon.t for
* traditional console interpreter, and in menuweb.t for the Web UI.
*
* Stephen Granade adapted this module from his TADS 2 menu system, and
* Mike Roberts made some minor cosmetic changes to integrate it with the
* main TADS 3 library.
*/
#include "advlite.h"
/*
* General instructions:
*
* Menus consist of MenuItems, MenuTopicItems, and MenuLongTopicItems.
*
* - MenuItems are the menu (and sub-menu) items that the player will
* select. Their "title" attribute is what will be shown in the menu,
* and the "heading" attribute is shown as the heading while the menu
* itself is active; by default, the heading simply uses the title.
*
* - MenuTopicItems are for lists of topic strings that the player will
* be shown, like hints. "title" is what will be shown in the menu;
* "menuContents" is a list of either strings to be displayed, one at a
* time, or objects which each must return a string via a "menuContents"
* method.
*
* - MenuLongTopicItems are for longer discourses. "title" is what will
* be shown in the menu; "menuContents" is either a string to be printed
* or a routine to be called.
*
* adv3.h contains templates for MenuItems, for your convenience.
*
* A simple example menu:
*
* FirstMenu: MenuItem 'Test menu';
*. + MenuItem 'Pets';
*. ++ MenuItem 'Chinchillas';
*. +++ MenuTopicItem 'About them'
*. menuContents = ['Furry', 'South American', 'Curious',
* 'Note: Not a coat'];
*. +++ MenuTopicItem 'Benefits'
*. menuContents = ['Non-allergenic', 'Cute', 'Require little space'];
*. +++ MenuTopicItem 'Downsides'
*. menuContents = ['Require dust baths', 'Startle easily'];
*. ++ MenuItem 'Cats';
*. +++ MenuLongTopicItem 'Pure evil'
*. menuContents = 'Cats are, quite simply, pure evil. I would provide
*. ample evidence were there room for it in this
*. simple example.';
*. +++ MenuTopicItem 'Benefits'
*. menuContents = ['They, uh, well...', 'Okay, I can\'t think of any.'];
*/
/* ------------------------------------------------------------------------ */
/*
* Menu output stream. We run topic contents through this output stream
* to allow topics to use the special paragraph and style tag markups.
*/
transient menuOutputStream: OutputStream
/*
* Process a function call through the stream. If the function
* generates any output, we capture it. If the function simply
* returns text, we run it through the filters.
*/
captureOutput(val)
{
/* reset our buffer */
buf_.deleteChars(1);
/* call the function while capturing its output */
outputManager.withOutputStream(menuOutputStream, function()
{
/* if it's a function, invoke it */
if (dataType(val) != TypeSString)
val = val();
/* if we have a string, run it through my filters */
if (dataType(val) == TypeSString)
writeToStream(val);
});
/* return my captured output */
return toString(buf_);
}
/* we capture our output to a string buffer */
writeFromStream(txt) { buf_.append(txt); }
/* initialize */
execute()
{
inherited();
buf_ = new StringBuffer();
addOutputFilter(typographicalOutputFilter);
addOutputFilter(menuParagraphManager);
addOutputFilter(styleTagFilter);
}
/* our capture buffer (a StringBuffer object) */
buf_ = nil
;
/*
* Paragraph manager for the menu output stream.
*/
transient menuParagraphManager: ParagraphManager
;
/* ------------------------------------------------------------------------ */
/*
* A basic menu object. This is an abstract base class that
* encapsulates some behavior common to different menu classes, and
* allows the use of the + syntax (like "+ MenuItem") to define
* containment.
*/
class MenuObject: object
/* our contents list */
contents = []
/*
* Since we're inheriting from object, but need to use the "+"
* syntax, we need to set up the contents appropriately
*/
initializeLocation()
{
if (location != nil)
location.addToContents(self);
}
/* add a menu item */
addToContents(obj)
{
/*
* If the menu has a nil menuOrder, and it inherits menuOrder
* from us, then it must be a dynamically-created object that
* doesn't provide a custom menuOrder. Provide a suitable
* default of a value one higher than the highest menuOrder
* currently in our list, to ensure that the item always sorts
* after any items currently in the list.
*/
if (obj.menuOrder == nil && !overrides(obj, MenuObject, &menuOrder))
{
local maxVal;
/* find the maximum current menuOrder value */
maxVal = nil;
foreach (local cur in contents)
{
/*
* if this one has a value, and it's the highest so far
* (or the only one with a value we've found so far),
* take it as the maximum so far
*/
if (cur.menuOrder != nil
&& (maxVal == nil || cur.menuOrder > maxVal))
maxVal = cur.menuOrder;
}
/* if we didn't find any values, use 0 as the arbitrary default */
if (maxVal == nil)
maxVal = 0;
/* go one higher than the maximum of the existing items */
obj.menuOrder = maxVal;
}
/* add the item to our contents list */
contents += obj;
}
/*
* The menu order. When we're about to show a list of menu items,
* we'll sort the list in ascending order of this property, then in
* ascending order of title. By default, we set this order value to
* be equal to the menu item's sourceTextOrder. This makes the menu
* order default to the order of objects as defined in the source. If
* some other basis is desired, override topicOrder.
*/
menuOrder = (sourceTextOrder)
/*
* Compare this menu object to another, for the purposes of sorting a
* list of menu items. Returns a positive number if this menu item
* sorts after the other one, a negative number if this menu item
* sorts before the other one, 0 if the relative order is arbitrary.
*
* By default, we'll sort by menuOrder if the menuOrder values are
* different, otherwise arbitrarily.
*/
compareForMenuSort(other)
{
/*
* if one menuOrder value is nil, sort it earlier than the other;
* if they're both nil, they sort as equivalent
*/
if (menuOrder == nil && other.menuOrder == nil)
return 0;
else if (menuOrder == nil)
return -1;
else if (other.menuOrder == nil)
return 1;
/* return the difference of the sort order values */
return menuOrder - other.menuOrder;
}
/*
* Finish initializing our contents list. This will be called on
* each MenuObject *after* we've called initializeLocation() on every
* object. In other words, every menu will already have been added
* to its parent's contents; this can do anything else that's needed
* to initialize the contents list. For example, some subclasses
* might want to sort their contents here, so that they list their
* menus in a defined order. By default, we sort the menu items by
* menuOrder; subclasses can override this as needed.
*/
initializeContents()
{
/* sort our contents list in the object-defined sorting order */
contents = contents.sort(
SortAsc, {a, b: a.compareForMenuSort(b)});
}
;
/*
* This preinit object makes sure the MenuObjects all have their
* contents initialized properly.
*/
PreinitObject
execute()
{
/* initialize each menu's location */
forEachInstance(MenuObject, { menu: menu.initializeLocation() });
/* do any extra work to initialize each menu's contents list */
forEachInstance(MenuObject, { menu: menu.initializeContents() });
}
;
/* ------------------------------------------------------------------------ */
/*
* A MenuItem is a given item in the menu tree. In general all you need
* to do to use menus is create a tree of MenuItems with titles.
*
* To display a menu tree, call displayMenu() on the top menu in the
* tree. That routine displays the menu and processes user input until
* the user dismisses the menu, automatically displaying submenus as
* necessary.
*/
class MenuItem: MenuObject
/* the name of the menu; this is listed in the parent menu */
title = ''
/*
* the heading - this is shown when this menu is active; by default,
* we simply use the title
*/
heading = (title)
/*
* Display properties. These properties control the way the menu
* appears on the screen. By default, a menu looks to its parent
* menu for display properties; this makes it easy to customize an
* entire menu tree, since changes in the top-level menu will cascade
* to all children that don't override these settings. However, each
* menu can customize its own appearance by overriding these
* properties itself.
*
* 'fgcolor' and 'bgcolor' are the foreground (text) and background
* colors, expressed as HTML color names (so '#nnnnnn' values can be
* used to specify RGB colors).
*
* 'indent' is the number of pixels to indent the menu's contents
* from the left margin. This is used only in HTML mode.
*
* 'fullScreenMode' indicates whether the menu should take over the
* entire screen, or limit itself to the space it actually requires.
* Full screen mode makes the menu block out any game window text.
* Limited mode leaves the game window partially uncovered, but can
* be a bit jumpy, since the window changes size as the user
* navigates through different menus.
*/
/* foreground (text) and background colors, as HTML color names */
fgcolor = (location != nil ? location.fgcolor : 'text')
bgcolor = (location != nil ? location.bgcolor : 'bgcolor')
/*
* Foreground and background colors for the top instructions bar.
* By default, we use the color scheme of the parent menu, or the
* inverse of our main menu color scheme if we're the top menu.
*/
topbarfg = (location != nil ? location.topbarfg : 'statustext')
topbarbg = (location != nil ? location.topbarbg : 'statusbg')
/* number of spaces to indent the menu's contents */
indent = (location != nil ? location.indent : '10')
/*
* full-screen mode: make our menu take up the whole screen (apart
* from the instructions bar, of course)
*/
fullScreenMode = (location != nil ? location.fullScreenMode : true)
/*
* The keys used to navigate the menus, in order:
*
* [quit, previous, up, down, select]
*
* Since multiple keys can be used for the same navigation, the list
* is implemented as a List of Lists. Keys must be given as
* lower-case in order to match input, since we convert all input
* keys to lower-case before matching them.
*
* In the sublist for each key, we use the first element as the key
* name we show in the instruction bar at the top of the screen.
*
* By default, we use our parent menu's key list, if we have a
* parent; if we have no parent, we use the standard keys from the
* library messages.
*/
keyList = (location != nil ? location.keyList : gLibMessages.menuKeyList)
/*
* the current key list - we'll set this on entry to the start of
* each showMenuXxx method, so that we keep track of the actual key
* list in use, as inherited from the top-level menu
*/
curKeyList = nil
/*
* Title for the link to the previous menu, if any. If the menu has
* a parent menu, we'll display this link next to the menu title in
* the top instructions/title bar. If this is nil, we won't display
* a link at all. Note that this can contain an HTML fragment; for
* example, you could use an <IMG> tag to display an icon here.
*/
prevMenuLink = (location != nil ? gLibMessages.prevMenuLink : nil)
/*
* Update our contents. By default, we'll do nothing; subclasses
* can override this to manage dynamic menus if desired. This is
* called just before the menu is displayed, each time it's
* displayed.
*/
updateContents() { }
/*
* Get the next menu in our list following the given menu. Returns
* nil if we don't find the given menu, or the given menu is the last
* menu.
*/
getNextMenu(menu)
{
/* find the menu in our contents list */
local idx = contents.indexOf(menu);
/*
* if we found it, and it's not the last, return the menu at the
* next index; otherwise return nil
*/
return (idx != nil && idx < contents.length()
? contents[idx + 1] : nil);
}
/*
* Get the menu previous tot he given menu. Returns nil if we don't
* find the given menu or the given menu is the first one.
*/
getPrevMenu(menu)
{
/* find the menu in our contents list */
local idx = contents.indexOf(menu);
/*
* if we found it, and it's not the first, return the menu at the
* prior index; otherwise return nil
*/
return (idx != nil && idx > 1 ? contents[idx - 1] : nil);
}
/* get the index in the parent of the given child menu */
getChildIndex(child)
{
return contents.indexOf(child);
}
;
/* ------------------------------------------------------------------------ */
/*
* MenuTopicItem displays a series of entries successively. This is
* intended to be used for displaying something like a list of hints for
* a topic. Set menuContents to be a list of strings to be displayed.
*/
class MenuTopicItem: MenuItem
/* the name of this topic, as it appears in our parent menu */
title = ''
/* heading, displayed while we're showing this topic list */
heading = (title)
/* hyperlink text for showing the next menu */
nextMenuTopicLink = (gLibMessages.nextMenuTopicLink)
/*
* A list of strings and/or MenuTopicSubItem items. Each one of
* these is meant to be something like a single hint on our topic.
* We display these items one at a time when our menu item is
* selected.
*/
menuContents = []
/* the index of the last item we displayed from our menuContents list */
lastDisplayed = 1
/*
* The maximum number of our sub-items that we'll display at once.
* This is only used on interpreters with banner capabilities, and is
* ignored in full-screen mode.
*/
chunkSize = 6
/* we'll display this after we've shown all of our items */
menuTopicListEnd = (gLibMessages.menuTopicListEnd)
;
/* ------------------------------------------------------------------------ */
/*
* A menu topic sub-item can be used to represent an item in a
* MenuTopicItem's list of display items. This can be useful when
* displaying a topic must trigger a side-effect.
*/
class MenuTopicSubItem: object
/*
* Get the item's text. By default, we just return an empty string.
* This should be overridden to return the appropriate text, and can
* also trigger any desired side-effects.
*/
getItemText() { return ''; }
;
/* ------------------------------------------------------------------------ */
/*
* Long Topic Items are used to print out big long gobs of text on a
* subject. Use it for printing long treatises on your design
* philosophy and the like.
*/
class MenuLongTopicItem: MenuItem
/* the title of the menu, shown in parent menus */
title = ''
/* the heading, shown while we're displaying our contents */
heading = (title)
/* either a string to be displayed, or a method returning a string */
menuContents = ''
/*
* Flag - this is a "chapter" in a list of chapters. If this is set
* to true, then we'll offer the options to proceed directly to the
* next and previous chapters. If this is nil, we'll simply wait for
* acknowledgment and return to the parent menu.
*/
isChapterMenu = nil
/* the message we display at the end of our text */
menuLongTopicEnd = (gLibMessages.menuLongTopicEnd)
;