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

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library: banner manager
7
 *   
8
 *   This module defines the banner manager, which provides high-level
9
 *   services to create and manipulate banner windows.
10
 *   
11
 *   A "banner" is an independent window shown within the interpreter's
12
 *   main application display frame (which might be the entire screen on a
13
 *   character-mode terminal, or could be a window in a GUI system).  The
14
 *   game can control the creation and destruction of banner windows, and
15
 *   can control their placement and size.
16
 *   
17
 *   This implementation is based in part on Steve Breslin's banner
18
 *   manager, used by permission.  
19
 */
20
21
#include "adv3.h"
22
23
/* ------------------------------------------------------------------------ */
24
/*
25
 *   A BannerWindow corresponds to an on-screen banner.  For each banner
26
 *   window a game wants to display, the game must create an object of this
27
 *   class.
28
 *   
29
 *   Note that merely creating a BannerWindow object doesn't actually
30
 *   display a banner window.  Once a BannerWindow is created, the game
31
 *   must call the object's showBanner() method to create the on-screen
32
 *   window for the banner.
33
 *   
34
 *   BannerWindow instances are intended to be persistent (not transient).
35
 *   The banner manager keeps track of each banner window that's actually
36
 *   being displayed separately via an internal transient object; the game
37
 *   doesn't need to worry about these tracking objects, since the banner
38
 *   manager automatically handles them.  
39
 */
40
class BannerWindow: object
41
    /*
42
     *   Construct the object.
43
     *   
44
     *   'id' is a globally unique identifying string for the banner.  When
45
     *   we dynamically create a banner object, we have to provide a unique
46
     *   identifying string, so that we can correlate transient on-screen
47
     *   banners with the banners in a saved state when restoring the saved
48
     *   state.
49
     *   
50
     *   Note that no ID string is needed for BannerWindow objects defined
51
     *   statically at compile-time, because the object itself ('self') is
52
     *   a suitably unique and stable identifier.  
53
     */
54
    construct(id)
55
    {
56
        /* remember my unique identifier */
57
        id_ = id;
58
    }
59
60
    /*
61
     *   Show the banner.  The game should call this method when it first
62
     *   wants to display the banner.
63
     *   
64
     *   'parent' is the parent banner; this is an existing BannerWindow
65
     *   object.  If 'parent' is nil, then the parent is the main game
66
     *   text window.  The new window's display space is obtained by
67
     *   carving space out of the parent's area, according to the
68
     *   alignment and size values specified.
69
     *   
70
     *   'where' and 'other' give the position of the banner among the
71
     *   children of the given parent.  'where' is one of the constants
72
     *   BannerFirst, BannerLast, BannerBefore, or BannerAfter.  If
73
     *   'where' is BannerBefore or BannerAfter, 'other' gives the
74
     *   BannerWindow object to be used as the reference point in the
75
     *   parent's child list; 'other' is ignored in other cases.  Note
76
     *   that 'other' must always be another child of the same parent; if
77
     *   it's not, then we act as though 'where' were given as BannerLast.
78
     *   
79
     *   'windowType' is a BannerTypeXxx constant giving the new window's
80
     *   type.
81
     *   
82
     *   'align' is a BannerAlignXxx constant giving the alignment of the
83
     *   new window.  'size' is an integer giving the size of the banner,
84
     *   in units specified by 'sizeUnits', which is a BannerSizeXxx
85
     *   constant.  If 'size' is nil, it indicates that the caller doesn't
86
     *   care about the size, usually because the caller will be resizing
87
     *   the banner soon anyway; the banner will initially have zero size
88
     *   in this case if we create a new window, or will retain the
89
     *   existing size if there's already a system window.
90
     *   
91
     *   'styleFlags' is a combination of BannerStyleXxx constants
92
     *   (combined with the bitwise OR operator, '|'), giving the requested
93
     *   display style of the new banner window.
94
     *   
95
     *   Note that if we already have a system banner window, and the
96
     *   existing banner window has the same characteristics as the new
97
     *   creation parameters, we'll simply re-use the existing window
98
     *   rather than closing and re-creating it; this reduces unnecessary
99
     *   redrawing in cases where the window isn't changing.  If the caller
100
     *   explicitly wants to create a new window even if we already have a
101
     *   window, the caller should simply call removeBanner() before
102
     *   calling this routine.  
103
     */
104
    showBanner(parent, where, other, windowType,
105
               align, size, sizeUnits, styleFlags)
106
    {
107
        local parentID;
108
        local otherID;
109
110
        /* note the ID's of the parent window and the insertion point */
111
        parentID = (parent != nil ? parent.getBannerID() : nil);
112
        otherID = (other != nil ? other.getBannerID() : nil);
113
114
        /* 
115
         *   if we have an 'other' specified, its parent must match our
116
         *   proposed parent; otherwise, ignore 'other' and insert at the
117
         *   end of the parent list 
118
         */
119
        if (other != nil && other.parentID_ != parentID)
120
        {
121
            other = nil;
122
            where = BannerLast;
123
        }
124
125
        /* if we already have an existing banner window, check for a match */
126
        if (handle_ != nil)
127
        {
128
            local t;
129
            local match;
130
131
            /* presume we won't find an exact match */
132
            match = nil;
133
134
            /* we already have a window - get the UI tracker object */
135
            if ((t = bannerUITracker.getTracker(self)) != nil)
136
            {
137
                /* check the placement, window type, alignment, and style */
138
                match = (t.windowType_ == windowType
139
                         && t.parentID_ == parentID
140
                         && t.align_ == align
141
                         && t.styleFlags_ == styleFlags
142
                         && bannerUITracker.orderMatches(t, where, otherID));
143
            }
144
145
            /* 
146
             *   if it doesn't match the existing window, close it, so that
147
             *   we will open a brand new window with the new
148
             *   characteristics 
149
             */
150
            if (!match)
151
                removeBanner();
152
        }
153
154
        /* if the system-level banner doesn't already exist, create it */
155
        if (handle_ == nil)
156
        {
157
            /* create my system-level banner window */
158
            if (!createSystemBanner(parent, where, other, windowType, align,
159
                                    size, sizeUnits, styleFlags))
160
            {
161
                /* we couldn't create the system banner - give up */
162
                return nil;
163
            }
164
165
            /* create our output stream */
166
            createOutputStream();
167
168
            /* add myself to the UI tracker's active banner list */
169
            bannerUITracker.addBanner(handle_, outputStream_, getBannerID(),
170
                                      parentID, where, other, windowType,
171
                                      align, styleFlags);
172
        }
173
        else
174
        {
175
            /* 
176
             *   Our system-level window already exists, so we don't need
177
             *   to create a new one.  However, our size could be
178
             *   different, so explicitly set the requested size if it
179
             *   doesn't match our recorded size.  If the size is given as
180
             *   nil, leave the size as it is; a nil size indicates that
181
             *   the caller doesn't care about the size (probably because
182
             *   the caller is going to change the size shortly anyway),
183
             *   so we can avoid unnecessary redrawing by leaving the size
184
             *   as it is for now.  
185
             */
186
            if (size != nil && (size != size_ || sizeUnits != sizeUnits_))
187
                bannerSetSize(handle_, size, sizeUnits, nil);
188
        }
189
190
        /* 
191
         *   remember the creation parameters, so that we can re-create the
192
         *   banner with the same characteristics in the future if we
193
         *   should need to restore the banner from a saved position 
194
         */
195
        parentID_ = parentID;
196
        windowType_ = windowType;
197
        align_ = align;
198
        size_ = size;
199
        sizeUnits_ = sizeUnits;
200
        styleFlags_ = styleFlags;
201
202
        /* 
203
         *   Add myself to the persistent banner tracker's active list.  Do
204
         *   this even if we already had a system handle, since we might be
205
         *   initializing the window as part of a persistent restore
206
         *   operation, in which case the persistent tracking object might
207
         *   not yet exist.  (This seems backwards: if we're restoring a
208
         *   persistent state, surely the persistent tracker would already
209
         *   exist.  In fact, the case we're really handling is where the
210
         *   window is open in the transient UI, because it was already
211
         *   open in the ongoing session; but the persistent state we're
212
         *   restoring doesn't include the window.  This is most likely to
213
         *   occur after a RESTART, since we could have a window that is
214
         *   always opened immediately at start-up and thus will be in the
215
         *   transient state up to and through the RESTART, but is only
216
         *   created as part of the initialization process.)  
217
         */
218
        bannerTracker.addBanner(self, parent, where, other);
219
220
        /* indicate success */
221
        return true;
222
    }
223
224
    /*
225
     *   Remove the banner.  This removes the banner's on-screen window.
226
     *   The BannerWindow object itself remains valid, but after this
227
     *   method returns, the BannerWindow no longer has an associated
228
     *   display window.
229
     *   
230
     *   Note that any child banners of ours will become undisplayable
231
     *   after we're gone.  A child banner depends upon its parent to
232
     *   obtain display space, so once the parent is gone, its children no
233
     *   longer have any way to obtain any display space.  Our children
234
     *   remain valid objects even after we're closed, but they won't be
235
     *   visible on the display.    
236
     */
237
    removeBanner()
238
    {
239
        /* if I don't have a system-level handle, there's nothing to do */
240
        if (handle_ == nil)
241
            return;
242
243
        /* remove my system-level banner window */
244
        bannerDelete(handle_);
245
246
        /* our system-level window is gone, so forget its handle */
247
        handle_ = nil;
248
249
        /* we only need an output stream when we're active */
250
        outputStream_ = nil;
251
252
        /* remove myself from the UI trackers's active list */
253
        bannerUITracker.removeBanner(getBannerID());
254
255
        /* remove myself from the persistent banner tracker's active list */
256
        bannerTracker.removeBanner(self);
257
    }
258
259
    /* write the given text to the banner */
260
    writeToBanner(txt)
261
    {
262
        /* write the text to our underlying output stream */
263
        outputStream_.writeToStream(txt);
264
    }
265
266
    /* 
267
     *   Invoke the given callback function, setting the default output
268
     *   stream to the banner's output stream for the duration of the
269
     *   call.  This allows invoking any code that writes to the current
270
     *   default output stream and displaying the result in the banner.  
271
     */
272
    captureOutput(func)
273
    {
274
        local oldStr;
275
        
276
        /* make my output stream the global default */
277
        oldStr = outputManager.setOutputStream(outputStream_);
278
279
        /* make sure we restore the default output stream on the way out */
280
        try
281
        {
282
            /* invoke the callback function */
283
            (func)();
284
        }
285
        finally
286
        {
287
            /* restore the original default output stream */
288
            outputManager.setOutputStream(oldStr);
289
        }
290
    }
291
292
    /* 
293
     *   Make my output stream the default in the output manager.  Returns
294
     *   the previous default output stream; the caller can note the return
295
     *   value and use it later to restore the original output stream via a
296
     *   call to outputManager.setOutputStream(), if desired.  
297
     */
298
    setOutputStream()
299
    {
300
        /* set my stream as the default */
301
        return outputManager.setOutputStream(outputStream_);
302
    }
303
304
    /* flush any pending output to the banner */
305
    flushBanner() { bannerFlush(handle_); }
306
307
    /*
308
     *   Set the banner window to a specific size.  'size' is the new
309
     *   size, in units given by 'sizeUnits', which is a BannerSizeXxx
310
     *   constant.
311
     *   
312
     *   'isAdvisory' is true or nil; if true, it indicates that the size
313
     *   setting is purely advisory, and that a sizeToContents() call will
314
     *   eventually follow to set the actual size.  When 'isAdvisory is
315
     *   true, the interpreter is free to ignore the request if
316
     *   sizeToContents() 
317
     */
318
    setSize(size, sizeUnits, isAdvisory)
319
    {
320
        /* set the underlying system window size */
321
        bannerSetSize(handle_, size, sizeUnits, isAdvisory);
322
323
        /* 
324
         *   remember my new size in case we have to re-create the banner
325
         *   from a saved state 
326
         */
327
        size_ = size;
328
        sizeUnits_ = sizeUnits;
329
    }
330
331
    /*
332
     *   Size the banner to its current contents.  Note that some systems
333
     *   do not support this operation, so callers should always make an
334
     *   advisory call to setSize() first to set a size based on the
335
     *   expected content size.  
336
     */
337
    sizeToContents()
338
    {
339
        /* size our system-level window to our contents */
340
        bannerSizeToContents(handle_);
341
    }
342
343
    /*
344
     *   Clear my banner window.  This clears out all of the contents of
345
     *   our on-screen display area.  
346
     */
347
    clearWindow()
348
    {
349
        /* clear our system-level window */
350
        bannerClear(handle_);
351
    }
352
353
    /* set the text color in the banner */
354
    setTextColor(fg, bg) { bannerSetTextColor(handle_, fg, bg); }
355
356
    /* set the screen color in the banner window */
357
    setScreenColor(color) { bannerSetScreenColor(handle_, color); }
358
359
    /* 
360
     *   Move the cursor to the given row/column position.  This can only
361
     *   be used with text-grid banners; for ordinary text banners, this
362
     *   has no effect. 
363
     */
364
    cursorTo(row, col) { bannerGoTo(handle_, row, col); }
365
366
    /*
367
     *   Get the banner identifier.  If our 'id_' property is set to nil,
368
     *   we'll assume that we're a statically-defined object, in which case
369
     *   'self' is a suitable identifier.  Otherwise, we'll return the
370
     *   identifier string. 
371
     */
372
    getBannerID() { return id_ != nil ? id_ : self; }
373
374
    /*
375
     *   Restore this banner.  This is called after a RESTORE or UNDO
376
     *   operation that finds that this banner was being displayed at the
377
     *   time the state was saved but is not currently displayed in the
378
     *   active UI.  We'll show the banner using the characteristics saved
379
     *   persistently.
380
     */
381
    showForRestore(parent, where, other)
382
    {
383
        /* show myself, using my saved characteristics */
384
        showBanner(parent, where, other, windowType_, align_,
385
                   size_, sizeUnits_, styleFlags_);
386
387
        /* update my contents */
388
        updateForRestore();
389
    }
390
391
    /*
392
     *   Create our output stream.  We'll create a BannerOutputStream and
393
     *   set it up with our default output filters.  Subclasses can
394
     *   override this as needed to customize the output stream. 
395
     */
396
    createOutputStream()
397
    {
398
        /* create a banner output stream */
399
        outputStream_ = new transient BannerOutputStream(handle_);
400
401
        /* set up the default filters */
402
        outputStream_.addOutputFilter(typographicalOutputFilter);
403
        outputStream_.addOutputFilter(new transient ParagraphManager());
404
        outputStream_.addOutputFilter(styleTagFilter);
405
        outputStream_.addOutputFilter(langMessageBuilder);
406
    }
407
408
    /*
409
     *   Create the system-level banner window.  This can be customized as
410
     *   needed, although this default implementation should be suitable
411
     *   for most instances.
412
     *   
413
     *   Returns true if we are successful in creating the system window,
414
     *   nil if we fail.  
415
     */
416
    createSystemBanner(parent, where, other, windowType, align,
417
                       size, sizeUnits, styleFlags)
418
    {
419
        /* create the system-level window */
420
        handle_ = bannerCreate(parent != nil ? parent.handle_ : nil,
421
                               where, other != nil ? other.handle_ : nil,
422
                               windowType, align, size, sizeUnits,
423
                               styleFlags);
424
425
        /* if we got a valid handle, we succeeded */
426
        return (handle_ != nil);
427
    }
428
429
    /*
430
     *   Update my contents after being restored.  By default, this does
431
     *   nothing; instances might want to override this to refresh the
432
     *   contents of the banner if the banner is normally updated only in
433
     *   response to specific events.  Note that it's not necessary to do
434
     *   anything here if the banner will soon be updated automatically as
435
     *   part of normal processing; for example, the status line banner is
436
     *   updated at each new command line via a prompt-daemon, so there's
437
     *   no need for the status line banner to do anything here.  
438
     */
439
    updateForRestore()
440
    {
441
        /* do nothing by default; subclasses can override as needed */
442
    }
443
444
    /*
445
     *   Initialize the banner window.  This is called during
446
     *   initialization (when first starting the game, or when resetting
447
     *   with RESTART).  If the banner is to be displayed from the start of
448
     *   the game, this can set up the on-screen display.
449
     *   
450
     *   Note that we might already have an on-screen handle when this is
451
     *   called.  This indicates that we're restarting an ongoing session,
452
     *   and that this banner already existed in the session before the
453
     *   RESTART operation.  If desired, we can attach ourselves to the
454
     *   existing on-screen banner, avoiding the redrawing that would occur
455
     *   if we created a new window.
456
     *   
457
     *   If this window depends upon another window for its layout order
458
     *   placement (i.e., we'll call showBanner() with another BannerWindow
459
     *   given as the 'other' parameter), then this routine should call the
460
     *   other window's initBannerWindow() method before creating its own
461
     *   window, to ensure that the other window has a system window and
462
     *   thus will be meaningful to establish the layout order.
463
     *   
464
     *   Overriding implementations should check the 'inited_' property.
465
     *   If this property is true, then it can be assumed that we've
466
     *   already been initialized and don't require further initialization.
467
     *   This routine can be called multiple times because dependent
468
     *   windows might call us directly, before we're called for our
469
     *   regular initialization.  
470
     */
471
    initBannerWindow()
472
    {
473
        /* by default, simply note that we've been initialized */
474
        inited_ = true;
475
    }
476
477
    /* flag: this banner has been initialized with initBannerWindow() */
478
    inited_ = nil
479
480
    /* 
481
     *   The creator-assigned ID string to identify the banner
482
     *   persistently.  This is only needed for banners created
483
     *   dynamically; for BannerWindow objects defined statically at
484
     *   compile time, simply leave this value as nil, and we'll use the
485
     *   object itself as the identifier.  
486
     */
487
    id_ = nil
488
489
    /* the handle to my system-level banner window */
490
    handle_ = nil
491
492
    /*
493
     *   My output stream - this is a transient OutputStream instance.
494
     *   We'll automatically create an output stream when we show the
495
     *   banner.  
496
     */
497
    outputStream_ = nil
498
499
    /* 
500
     *   Creation parameters.  We store these when we create the banner,
501
     *   and update them as needed when the banner's display attributes
502
     *   are changed.  
503
     */
504
    parentID_ = nil
505
    windowType_ = nil
506
    align_ = nil
507
    size_ = nil
508
    sizeUnits_ = nil
509
    styleFlags_ = nil
510
;
511
512
/* ------------------------------------------------------------------------ */
513
/*
514
 *   Banner Output Stream.  This is a specialization of OutputStream that
515
 *   writes to a banner window.  
516
 */
517
class BannerOutputStream: OutputStream
518
    /* construct */
519
    construct(handle)
520
    {
521
        /* inherit base class constructor */
522
        inherited();
523
        
524
        /* remember my banner window handle */
525
        handle_ = handle;
526
    }
527
528
    /* execute preinitialization */
529
    execute()
530
    {
531
        /*
532
         *   We shouldn't need to do anything during pre-initialization,
533
         *   since we should always be constructed dynamically by a
534
         *   BannerWindow.  Don't even inherit the base class
535
         *   initialization, since it could clear out state that we want to
536
         *   keep through a restart, restore, etc.  
537
         */
538
    }
539
540
    /* write text from the stream to the interpreter I/O system */
541
    writeFromStream(txt)
542
    {
543
        /* write the text to the underlying system banner window */
544
        bannerSay(handle_, txt);
545
    }
546
547
    /* our system-level banner window handle */
548
    handle_ = nil
549
;
550
551
552
/* ------------------------------------------------------------------------ */
553
/*
554
 *   The banner UI tracker.  This object keeps track of the current user
555
 *   interface display state; this object is transient because the
556
 *   interpreter's user interface is not part of the persistence
557
 *   mechanism.  
558
 */
559
transient bannerUITracker: object
560
    /* add a banner to the active display list */
561
    addBanner(handle, ostr, id, parentID, where, other,
562
              windowType, align, styleFlags)
563
    {
564
        local uiWin;
565
        local parIdx;
566
        local idx;
567
568
        /* create a transient BannerUIWindow object to track the banner */
569
        uiWin = new transient BannerUIWindow(handle, ostr, id, parentID,
570
                                             windowType, align, styleFlags);
571
572
        /* 
573
         *   Find the parent in the list.  If there's no parent, the
574
         *   parent is the main window; consider it to be at imaginary
575
         *   index zero in the list. 
576
         */
577
        parIdx = (parentID == nil
578
                  ? 0 : activeBanners_.indexWhich({x: x.id_ == parentID}));
579
580
        /* insert the banner at the proper point in our list */
581
        switch(where)
582
        {
583
        case BannerFirst:
584
            /* 
585
             *   insert as the first child of the parent - put it
586
             *   immediately after the parent in the list 
587
             */
588
            activeBanners_.insertAt(parIdx + 1, uiWin);
589
            break;
590
591
        case BannerLast:
592
        ins_last:
593
            /* 
594
             *   Insert as the last child of the parent: insert
595
             *   immediately after the last window that descends from the
596
             *   parent.  
597
             */
598
            activeBanners_.insertAt(skipDescendants(parIdx), uiWin);
599
            break;
600
601
        case BannerBefore:
602
        case BannerAfter:
603
            /* find the reference point ID in our list */
604
            idx = activeBanners_.indexWhich(
605
                {x: x.id_ == other.getBannerID()});
606
607
            /* 
608
             *   if we didn't find the reference point, or the reference
609
             *   point item doesn't have the same parent as the new item,
610
             *   then ignore the reference point and instead insert at the
611
             *   end of the parent's child list 
612
             */
613
            if (idx == nil || activeBanners_[idx].parentID_ != parentID)
614
                goto ins_last;
615
616
            /* 
617
             *   if inserting after, skip the reference item and all
618
             *   of its descendants 
619
             */
620
            if (where == BannerAfter)
621
                idx = skipDescendants(idx);
622
623
            /* insert at the position we found */
624
            activeBanners_.insertAt(idx, uiWin);
625
            break;
626
        }
627
    }
628
629
    /*
630
     *   Given an index in our list of active windows, skip the given item
631
     *   and all items whose windows are descended from this window.
632
     *   We'll leave the index positioned on the next entry in the list
633
     *   that isn't a descendant of the window at the given index.  Note
634
     *   that this skips not only children but grandchildren (and so on)
635
     *   as well.  
636
     */
637
    skipDescendants(idx)
638
    {
639
        local parentID;
640
641
        /* 
642
         *   if the index is zero, it's the main window; all windows are
643
         *   children of the root window, so return the next index after
644
         *   the last item 
645
         */
646
        if (idx == 0)
647
            return activeBanners_.length() + 1;
648
649
        /* note ID of the parent item */
650
        parentID = activeBanners_[idx].id_;
651
        
652
        /* skip the parent item */
653
        ++idx;
654
655
        /* keep going as long as we see children of the parent */
656
        while (idx <= activeBanners_.length()
657
               && activeBanners_[idx].parentID_ == parentID)
658
        {
659
            /* 
660
             *   This is a child of the given parent, so we must skip it;
661
             *   we must also skip its descendants, since they're all
662
             *   indirectly descendants of the original parent.  So,
663
             *   simply skip this item and its descendants with a
664
             *   recursive call to this routine.
665
             */
666
            idx = skipDescendants(idx);
667
        }
668
669
        /* return the new index */
670
        return idx;
671
    }
672
673
    /* remove a banner from the active display list */
674
    removeBanner(id)
675
    {
676
        local idx;
677
678
        /* find the entry with the given ID, and remove it */
679
        if ((idx = activeBanners_.indexWhich({x: x.id_ == id})) != nil)
680
        {
681
            local lastIdx;
682
            
683
            /* 
684
             *   After removing an item, its children are no longer
685
             *   displayable, because a child obtains display space from
686
             *   its parent.  So, we must remove any children of this item
687
             *   at the same time we remove the item itself.  Find the
688
             *   index of the next item after all of our descendants, so
689
             *   that we can remove the item and its children all at once.
690
             *   An item and its descendants are always contiguous in our
691
             *   list, since we store children immediately after their
692
             *   parents, so we can simply remove the range of items from
693
             *   the specified item to its last descendant.
694
             *   
695
             *   Note that skipDescendants() returns the index of the
696
             *   first item that is NOT a descendant; so, decrement the
697
             *   result so that we end up with the index of the last
698
             *   descendant.  
699
             */
700
            lastIdx = skipDescendants(idx) - 1;
701
702
            /* remove the item and all of its children */
703
            activeBanners_.removeRange(idx, lastIdx);
704
        }
705
    }
706
707
    /* get the BannerUIWindow tracker object for a given BannerWindow */
708
    getTracker(win)
709
    {
710
        local id;
711
712
        /* get the window's ID */
713
        id = win.getBannerID();
714
        
715
        /* return the tracker with the same ID as the given BannerWindow */
716
        return activeBanners_.valWhich({x: x.id_ == id});
717
    }
718
719
    /* check a BannerUIWindow to see if it matches the given layout order */
720
    orderMatches(uiWin, where, otherID)
721
    {
722
        local idx;
723
        local otherIdx;
724
        local parentID;
725
        local parIdx;
726
727
        /* get the list index of the given window */
728
        idx = activeBanners_.indexOf(uiWin);
729
730
        /* get the list index of the reference point window */
731
        otherIdx = (otherID != nil
732
                    ? activeBanners_.indexWhich({x: x.id_ == otherID}) : nil);
733
734
        /* 
735
         *   find the parent item (using imaginary index zero for the
736
         *   root, which we can think of as being just before the first
737
         *   item in the list)
738
         */
739
        parentID = uiWin.parentID_;
740
        parIdx = (parentID == nil
741
                  ? 0 : activeBanners_.indexWhich({x: x.id_ == parentID}));
742
743
        /* 
744
         *   if 'other' is specified, it has to have our same parent; if
745
         *   it has a different parent, it's not a match 
746
         */
747
        if (otherID != nil && parentID != activeBanners_[otherIdx].parentID_)
748
            return nil;
749
750
        /* 
751
         *   if there's no such window in the list, it can't match the
752
         *   given placement no matter what the given placement is, as it
753
         *   has no placement 
754
         */
755
        if (idx == nil)
756
            return nil;
757
        
758
        /* check the requested layout order */
759
        switch (where)
760
        {
761
        case BannerFirst:
762
            /* make sure it's immediately after the parent */
763
            return idx == parIdx + 1;
764
765
        case BannerLast:
766
            /* 
767
             *   Make sure it's the last child of the parent.  To do this,
768
             *   make sure that the next item after this item's last
769
             *   descendant is the same as the next item after the
770
             *   parent's last descendant. 
771
             */
772
            return skipDescendants(idx) == skipDescendants(parIdx);
773
774
        case BannerBefore:
775
            /* 
776
             *   we want this item to come before 'other', so make sure
777
             *   the next item after all of this item's descendants is
778
             *   'other' 
779
             */
780
            return skipDescendants(idx) == otherIdx;
781
782
        case BannerAfter:
783
            /* 
784
             *   we want this item to come just after 'other', so make
785
             *   sure that the next item after all of the descendants of
786
             *   'other' is this item 
787
             */
788
            return skipDescendants(otherIdx) == idx;
789
790
        default:
791
            /* other layout orders are invalid */
792
            return nil;
793
        }
794
    }
795
796
    /*
797
     *   The vector of banners currently on the screen.  This is a list of
798
     *   transient BannerUIWindow objects, stored in the same order as the
799
     *   banner layout list.  
800
     */
801
    activeBanners_ = static new transient Vector(32)
802
;
803
804
/*
805
 *   A BannerUIWindow object.  This keeps track of the transient UI state
806
 *   of a banner window while it appears on the screen.  We create only
807
 *   transient instances of this class, since it tracks what's actually
808
 *   displayed at any given time.  
809
 */
810
class BannerUIWindow: object
811
    /* construct */
812
    construct(handle, ostr, id, parentID, windowType, align, styleFlags)
813
    {
814
        /* remember the banner's data */
815
        handle_ = handle;
816
        outputStream_ = ostr;
817
        id_ = id;
818
        parentID_ = parentID;
819
        windowType_ = windowType;
820
        align_ = align;
821
        styleFlags_ = styleFlags;
822
    }
823
824
    /* the system-level banner handle */
825
    handle_ = nil
826
827
    /* the banner's ID */
828
    id_ = nil
829
830
    /* the parent banner's ID (nil if this is a top-level banner) */
831
    parentID_ = nil
832
833
    /* 
834
     *   The banner's output stream.  Output streams are always transient,
835
     *   so hang on to each active banner's stream so that we can plug it
836
     *   back in on restore. 
837
     */
838
    outputStream_ = nil
839
840
    /* creation parameters of the banner */
841
    windowType_ = nil
842
    align_ = nil
843
    styleFlags_ = nil
844
845
    /* 
846
     *   Scratch-pad for our association to our BannerWindow object.  We
847
     *   only use this during the RESTORE process, to tie the transient
848
     *   object back to the proper persistent object. 
849
     */
850
    win_ = nil
851
;
852
853
/*
854
 *   The persistent banner tracker.  This keeps track of the active banner
855
 *   windows persistently.  Whenever we save or restore the game's state,
856
 *   this object will be saved or restored along with the state.  When we
857
 *   restore a previously saved state, we can look at this object to
858
 *   determine which banners were active at the time the state was saved,
859
 *   and use this information to restore the same active banners in the
860
 *   user interface.
861
 *   
862
 *   This is a post-restore and post-undo object, so we're notified via our
863
 *   execute() method whenever we restore a saved state using RESTORE or
864
 *   UNDO.  When we restore a saved state, we'll restore the banner display
865
 *   conditions as they existed in the saved state.  
866
 */
867
bannerTracker: PostRestoreObject, PostUndoObject
868
    /* add a banner to the active display list */
869
    addBanner(win, parent, where, other)
870
    {
871
        local parIdx;
872
        local otherIdx;
873
        
874
        /* 
875
         *   Don't add it if it's already in the list.  If we're restoring
876
         *   the banner from persistent state, it'll already be in the
877
         *   active list, since the active list is the set of windows
878
         *   we're restoring in the first place. 
879
         */
880
        if (activeBanners_.indexOf(win) != nil)
881
            return;
882
883
        /* find the parent among the existing windows */
884
        parIdx = (parent == nil ? 0 : activeBanners_.indexOf(parent));
885
886
        /* note the index of 'other' */
887
        otherIdx = (other == nil ? nil : activeBanners_.indexOf(other));
888
889
        /* insert the banner at the proper point in our list */
890
        switch(where)
891
        {
892
        case BannerFirst:
893
            /* insert immediately after the parent */
894
            activeBanners_.insertAt(parIdx + 1, win);
895
            break;
896
897
        case BannerLast:
898
        ins_last:
899
            /* insert after the parent's last descendant */
900
            activeBanners_.insertAt(skipDescendants(parIdx), win);
901
            break;
902
903
        case BannerBefore:
904
        case BannerAfter:
905
            /* 
906
             *   if we didn't find the reference point, insert at the end
907
             *   of the parent's child list 
908
             */
909
            if (otherIdx == nil)
910
                goto ins_last;
911
912
            /* 
913
             *   if inserting after, skip the reference item and all of
914
             *   its descendants 
915
             */
916
            if (where == BannerAfter)
917
                otherIdx = skipDescendants(otherIdx);
918
919
            /* insert at the position we found */
920
            activeBanners_.insertAt(otherIdx, win);
921
            break;
922
        }
923
    }
924
925
    /*
926
     *   Skip all descendants of the window at the given index. 
927
     */
928
    skipDescendants(idx)
929
    {
930
        local parentID;
931
932
        /* index zero is the root item, so skip the entire list */
933
        if (idx == 0)
934
            return activeBanners_.length() + 1;
935
        
936
        /* note the parent item */
937
        parentID = activeBanners_[idx].getBannerID();
938
939
        /* skip the parent item */
940
        ++idx;
941
942
        /* keep going as long as we see children of the parent */
943
        while (idx < activeBanners_.length()
944
               && activeBanners_[idx].parentID_ == parentID)
945
        {
946
            /* this is a child, so skip it and all of its descendants */
947
            idx = skipDescendants(idx);
948
        }
949
950
        /* return the new index */
951
        return idx;
952
    }
953
954
    /* remove a banner from the active list */
955
    removeBanner(win)
956
    {
957
        local idx;
958
        local lastIdx;
959
960
        /* get the index of the item to remove */
961
        idx = activeBanners_.indexOf(win);
962
963
        /* if we didn't find it, ignore the request */
964
        if (idx == nil)
965
            return;
966
967
        /* find the index of its last descendant */
968
        lastIdx = skipDescendants(idx) - 1;
969
970
        /* 
971
         *   remove the item and all of its descendants - child items
972
         *   cannot be displayed once their parents are gone, so we can
973
         *   remove all of this item's children, all of their children,
974
         *   and so on, as they are becoming undisplayable 
975
         */
976
        activeBanners_.removeRange(idx, lastIdx);
977
    }
978
979
    /*
980
     *   The list of active banners.  This is a list of BannerWindow
981
     *   objects, stored in banner layout list order. 
982
     */
983
    activeBanners_ = static new Vector(32)
984
985
    /* receive RESTORE/UNDO notification */
986
    execute()
987
    {
988
        /* restore the display state for a non-initial state */
989
        restoreDisplayState(nil);
990
    }
991
992
    /*
993
     *   Restore the saved banner display state, so that the banner layout
994
     *   looks the same as it did when we saved the persistent state.  This
995
     *   should be called after restoring a saved state, undoing to a
996
     *   savepoint, or initializing (when first starting the game or when
997
     *   restarting).
998
     */
999
    restoreDisplayState(initing)
1000
    {
1001
        local uiVec;
1002
        local uiIdx;
1003
        local origActive;
1004
1005
        /* get the list of banners active in the UI */
1006
        uiVec = bannerUITracker.activeBanners_;
1007
1008
        /*
1009
         *   First, go through all of the persistent BannerWindow objects.
1010
         *   For each one whose ID shows up in the active UI display list,
1011
         *   tell the BannerWindow object its current UI handle.  
1012
         */
1013
        forEachInstance(BannerWindow, new function(cur)
1014
        {
1015
            local uiCur;
1016
            
1017
            /* find this banner in the active UI list */
1018
            uiCur = uiVec.valWhich({x: x.id_ == cur.getBannerID()});
1019
1020
            /* 
1021
             *   if the window exists in the active UI list, note the
1022
             *   current system handle for the window; otherwise, we have
1023
             *   no system window, so set the handle to nil 
1024
             */
1025
            if (uiCur != nil)
1026
            {
1027
                /* note the current system banner handle */
1028
                cur.handle_ = uiCur.handle_;
1029
1030
                /* re-establish the banner's active output stream */
1031
                cur.outputStream_ = uiCur.outputStream_;
1032
1033
                /* tie the transient record to the current 'cur' */
1034
                uiCur.win_ = cur;
1035
            }
1036
            else
1037
            {
1038
                /* it's not shown, so it has no system banner handle */
1039
                cur.handle_ = nil;
1040
1041
                /* it has no output stream */
1042
                cur.outputStream_ = nil;
1043
            }
1044
        });
1045
1046
        /* 
1047
         *   
1048
         *   'initing' indicates whether we're initializing (startup or
1049
         *   RESTART) or doing something else (RESTORE, UNDO).  When
1050
         *   initializing, if there are any banners on-screen, we'll give
1051
         *   their associated BannerWindow objects (if any) a chance to set
1052
         *   up their initial conditions; this allows us to avoid
1053
         *   unnecessary redrawing if we have banners that we'd immediately
1054
         *   set up to the same conditions anyway, since we can just keep
1055
         *   the existing banners rather than removing and re-creating
1056
         *   them.
1057
         *   
1058
         *   So, if we're initializing, tell each banner that it's time to
1059
         *   set up its initial display.  
1060
         */
1061
        if (initing)
1062
            forEachInstance(BannerWindow, {cur: cur.initBannerWindow()});
1063
1064
        /* 
1065
         *   scan the active UI list, and close each window that isn't
1066
         *   still open in the saved state 
1067
         */
1068
        foreach (local uiCur in uiVec)
1069
        {
1070
            /* if this window isn't in the active list, close it */
1071
            if (activeBanners_.indexWhich(
1072
                {x: x.getBannerID() == uiCur.id_}) == nil)
1073
            {
1074
                /*
1075
                 *   There's no banner in the persistent list with this
1076
                 *   ID, so this window is not part of the state we're
1077
                 *   restoring.  Close the window.  If we have an
1078
                 *   associated BannerWindow object, close through the
1079
                 *   window object; otherwise, close the system handle
1080
                 *   directly.  
1081
                 */
1082
                if (uiCur.win_ != nil)
1083
                {
1084
                    /* we have a BannerWindow - close it */
1085
                    uiCur.win_.removeBanner();
1086
                }
1087
                else
1088
                {
1089
                    /* there's no BannerWindow - close the system window */
1090
                    bannerDelete(uiCur.handle_);
1091
1092
                    /* remove the UI tracker object */
1093
                    uiVec.removeElement(uiCur);
1094
                }
1095
            }
1096
        }
1097
1098
        /* start at the first banner actually displayed right now */
1099
        uiIdx = 1;
1100
1101
        /* 
1102
         *   make a copy of the original active list - we might modify the
1103
         *   actual active list in the course of restoring things, so make
1104
         *   a copy that we can refer to as we reconstruct the original
1105
         *   list 
1106
         */
1107
        origActive = activeBanners_.toList();
1108
1109
        /* 
1110
         *   Scan the saved list of banners, and restore each one.  Note
1111
         *   that by restoring windows in the order in which they appear
1112
         *   in the list, we ensure that we always restore a parent before
1113
         *   restoring any of its children, since a child always follows
1114
         *   its parent in the list.  
1115
         */
1116
        for (local curIdx = 1, local aLen = origActive.length() ;
1117
             curIdx <= aLen ; ++curIdx)
1118
        {
1119
            local redisp;
1120
            local cur;
1121
1122
            /* get the current item */
1123
            cur = origActive[curIdx];
1124
                
1125
            /* presume we will have to redisplay this banner */
1126
            redisp = true;
1127
            
1128
            /*
1129
             *   If this banner matches the current banner in the active
1130
             *   UI display list, and the characteristics match, we need
1131
             *   do nothing, as we're already displaying this banner
1132
             *   properly.  If the current active UI banner doesn't match,
1133
             *   then we need to insert this saved banner at the current
1134
             *   active UI position. 
1135
             */
1136
            if (uiVec.length() >= uiIdx)
1137
            {
1138
                local uiCur;
1139
1140
                /* get this current UI display item (a BannerUIWindow) */
1141
                uiCur = uiVec[uiIdx];
1142
1143
                /* check for a match to 'cur' */
1144
                if (uiCur.id_ == cur.getBannerID()
1145
                    && uiCur.parentID_ == cur.parentID_
1146
                    && uiCur.windowType_ == cur.windowType_
1147
                    && uiCur.align_ == cur.align_
1148
                    && uiCur.styleFlags_ == cur.styleFlags_)
1149
                {
1150
                    /*
1151
                     *   This saved banner ('cur') exactly matches the
1152
                     *   active UI banner ('uiCur') at the same position
1153
                     *   in the layout list.  Therefore, we do not need to
1154
                     *   redisplay 'cur'.
1155
                     */
1156
                    redisp = nil;
1157
                }
1158
            }
1159
1160
            /* if we need to redisplay 'cur', do so */
1161
            if (redisp)
1162
            {
1163
                local prvIdx;
1164
                local where;
1165
                local other;
1166
                local parent;
1167
1168
                /*   
1169
                 *   If 'cur' is already being displayed, we must remove
1170
                 *   it before showing it anew.  This is the only way to
1171
                 *   ensure that we display it with the proper
1172
                 *   characteristics, since the characteristics of the
1173
                 *   current instance of its window don't match up to what
1174
                 *   we want to restore.  
1175
                 */
1176
                if (cur.handle_ != nil)
1177
                    cur.removeBanner();
1178
1179
                /*
1180
                 *   Figure out how to specify this window's display list
1181
                 *   position.  A display list position is always
1182
                 *   specified relative to the parent's child list, so
1183
                 *   figure out where we go in our parent's list.  Scan
1184
                 *   backwards in the active list for the nearest previous
1185
                 *   window with the same parent.  If we find one, insert
1186
                 *   the new window after that prior sibling; otherwise,
1187
                 *   insert as the first child of our parent.  Presume
1188
                 *   that we'll fail to find a prior sibling, then search
1189
                 *   for it and search for our parent.  
1190
                 */
1191
                where = BannerFirst;
1192
                other = nil;
1193
                for (prvIdx = curIdx - 1 ; prvIdx > 0 ; --prvIdx)
1194
                {
1195
                    local prv;
1196
1197
                    /* note this item */
1198
                    prv = origActive[prvIdx];
1199
                    
1200
                    /* 
1201
                     *   If this item has our same parent, and we haven't
1202
                     *   already found a prior sibling, this is our most
1203
                     *   recent prior sibling, so note it.  
1204
                     */
1205
                    if (where == BannerFirst
1206
                        && prv.parentID_ == cur.parentID_)
1207
                    {
1208
                        /* insert after this prior sibling */
1209
                        where = BannerAfter;
1210
                        other = prv;
1211
                    }
1212
1213
                    /* if this is our parent, note it */
1214
                    if (prv.getBannerID() == cur.parentID_)
1215
                    {
1216
                        /* 
1217
                         *   note the parent BannerWindow object - we'll
1218
                         *   need it to specify our window display
1219
                         *   position 
1220
                         */
1221
                        parent = prv;
1222
1223
                        /* 
1224
                         *   Children of a given parent always come after
1225
                         *   the parent in the display list, so there's no
1226
                         *   possibility of finding another sibling.
1227
                         *   There's also obviously no possibility of
1228
                         *   finding another parent.  So, our work here is
1229
                         *   done; we can stop scanning.
1230
                         */
1231
                        break;
1232
                    }
1233
                }
1234
                    
1235
                /* show the window */
1236
                cur.showForRestore(parent, where, other);
1237
            }
1238
            else
1239
            {
1240
                /* 
1241
                 *   the banner is already showing, so we don't need to
1242
                 *   redisplay it; simply notify it that a Restore
1243
                 *   operation has taken place so that it can do any
1244
                 *   necessary updates 
1245
                 */
1246
                cur.updateForRestore();
1247
            }
1248
1249
            /*
1250
             *   'cur' should now be displayed, but we might have failed
1251
             *   to re-create it.  If we did show the window, we can
1252
             *   advance to the next slot in the UI list, since this
1253
             *   window will necessarily be at the current spot in the UI
1254
             *   list.  
1255
             */
1256
            if (cur.handle_ != nil)
1257
            {
1258
                /* 
1259
                 *   We know that this window is now the entry in the
1260
                 *   active UI list at the current index we're looking at.
1261
                 *   Move on to the next position in the active list for
1262
                 *   the next saved window.  
1263
                 */
1264
                ++uiIdx;
1265
            }
1266
        }
1267
    }
1268
;
1269
1270
/*
1271
 *   Initialization object - this will be called when we start the game the
1272
 *   first time or RESTART within a session.  We'll restore the display
1273
 *   state to the initial conditions. 
1274
 */
1275
bannerInit: InitObject
1276
    execute()
1277
    {
1278
        /* restore banner displays to their initial conditions */
1279
        bannerTracker.restoreDisplayState(true);
1280
    }
1281
;