cfad47cfa3/t3compiler/tads3/lib/extensions/customBanner.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
#include <adv3.h>
3
#include <en_us.h>
4
5
  /*
6
   *    Custom Banner version 1.2
7
   *     by Eric Eve
8
   *  
9
   *     Version date: 13-Sep-06
10
   *
11
   *     This file implements a CustomBannerWindow class that vastly eases
12
   *     the process of setting up banners and displaying material in them.
13
   *     e.g. to set up a graphics banner to display pictures, starting with
14
   *     pic1.jpg at startup, but not appearing at all on an interpreter that
15
   *     can't display JPEGs you could define:
16
   *
17
   *     pictureWindow: CustomBanner
18
   *       canDisplay = (systemInfo(SysInfoJpeg))
19
   *       bannerArgs = [nil, BannerAfter,  statuslineBanner, BannerTypeText, 
20
   *             BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder]
21
   *       currentContents = '<img src="pic1.jpg">'
22
   *    ;
23
   *
24
   *    Then to change the picture dislayed at a later point, call:
25
   *
26
   *       pictureWindow.updateContents('<img src="pic2.jpg">');
27
   *
28
   *    And everything else, including getting everything right on RESTART, UNDO
29
   *    and RESTORE should be taken care of.
30
   */
31
32
  /* ------------------------------------------------------------------------ */
33
  /* 
34
   *  A CustomBannerWindow, like a BannerWindow, corrsponds to an on-screen
35
   *  banner. The purpose of CustomBannerWindow is to eliminate most of the
36
   *  busy-work that a game author would otherwise have to take care of in
37
   *  displaying and manipulating banners.
38
   *
39
   *  As with BannerWinnow, merely creating a CustomBannerWindow does not
40
   *  display the banner. However, any CustomBannerWindows in existence at
41
   *  the start of the game will be added to the screen display, unless the
42
   *  condition specified in their shouldDisplay() method prevents initialization.
43
   *
44
   *  The one property that must be defined on each instance of a CustomBannerWindow
45
   *  is bannerArgs, which takes the form:
46
   *
47
   *     bannerArgs = [parent, where, other, windowType, align, 
48
   *                     size, sizeUnits, styleFlags]
49
   *
50
   *  where each list element has the same meaning at the corresponding argument
51
   *  to BannerWindow.showBanner()
52
   *
53
   *  This merely ensures that the CustomBannerWindow is added to the screen's
54
   *  banner window layout. To have the CustomBannerWindow display some content
55
   *  when first added to the screen layout, override its current contents property:
56
   *
57
   *     currentContents = 'My initial contents'
58
   *
59
   *  To change what's displayed in a CustomBannerWindow from game code, call its
60
   *  updateContents() method, e.g.:
61
   *
62
   *     pictureWindow.updateContents('<img src="pics/troll.jpg">');
63
   *
64
   *  To redisplay the current contents, call the showCurrentContents() method.
65
   *  By default a call to updateContents() or showCurrentContents() clears the
66
   *  window before displaying the new content. To have the additional content
67
   *  added to the existing content, change the clearBeforeUpdate property to nil.
68
   *
69
   *  You can control whether the game uses this banner at all by overriding
70
   *  the canDisplay property. The main purpose of this property is to easily allow
71
   *  a game to run on different types of interpreter. For example, if your banner is
72
   *  meant to display pictures in the JPEG format, there's no point having it included
73
   *  in the screen layout of an interpreter that can't display JPEGs, and attempts to
74
   *  update its contents with a new picture should do nothing. In which case we could
75
   *  define:
76
   *
77
   *     canDisplay = (systemInfo(SysInfoJpeg))
78
   *
79
   *  Calls to CustomBannerWindow methods like updateContents() and clearWindow()
80
   *  should be safe on an interpreter for which shouldDisplay returns nil, since
81
   *  by default these will do nothing beyond updating the banner's currentContents
82
   *  property. This makes it easier to write game code that is suitable for all 
83
   *  classes of interpreter
84
   *
85
   *  To have a CustomBannerWindow resize to contents each time its contents are
86
   *  displayed, set its autoSize property to true.   
87
   *
88
   *  If you do not want a CustomBannerWindow you have defined not to be dispayed
89
   *  at game startup, set its isActive property to nil. Call the activate()
90
   *  method to add it to the screen layout and the deactivate() method to remove
91
   *  it, or any other CustomBannerWindow, from the screen display.    
92
   *    
93
   *  Obviously, it is then the game author's responsibility to ensure that no
94
   *  other banner window that's meant to persist after pictureWindow is deactivated
95
   *  depends on pictureWindow for its existence; i.e. that we're not deactivating
96
   *  the parent of some other banner window(s) we want to keep or subsequently
97
   *  activate, or the sibling of any banner window that's subsequently going to
98
   *  defined in relation to us.    
99
   */
100
101
class CustomBannerWindow: BannerWindow
102
103
 /*
104
  *  The list of any banner windows that must be set up before me,
105
  *  either one of them is my parent, or because I'm going
106
  *  to be placed before or after them with BannerBefore or BannerAfter.
107
  *
108
  *  If bannerArgs has been set up with the list of showBanner arguments, 
109
  *  then we can derive this information automatically
110
  */
111
  initBeforeMe()
112
  {
113
    /*
114
     *  If our bannerArgs property contains a list of the right length, i.e. 8
115
     *  elements, then the first and third elements of the list (our parent, and
116
     *  the sibling we're to be placed before or after) must be initialized before
117
     *  we are. If either of these is nil, no harm is done, since initBannerWindow()
118
     *  will simply skip the nil value.     
119
     *
120
     *  Moreover, if our sibling is in the list, we don't need our parent as well,
121
     *  since either our sibling or one of its siblings will initialize our parent.
122
     */
123
     
124
    local lst = [];
125
    
126
    if (propType(&bannerArgs) == TypeList && bannerArgs.length() == 8) 
127
      lst = bannerArgs[3] ? [bannerArgs[3]] : [bannerArgs[1]];
128
        
129
    initBeforeMe = lst;
130
    
131
    return lst;    
132
  }   
133
134
  
135
  /*
136
   * A condition to test whether this banner window should actually display.
137
   * Normally this would test for the interpreter class if this would
138
   * affect whether we wanted this banner to be created. For example, if
139
   * we were going to use this banner window to display a JPEG picture, we
140
   * might not this window to display at all if the interpreter we're running
141
   * on can't display JPEGS, so we might write:
142
   *
143
   *    canDisplay = (systemInfo(SysInfoJpeg))
144
   *
145
   *   If your complete system of CustomBanners depends on the same condition
146
   *   (e.g. you don't want any CustomBanners if the interpreter we're running
147
   *   on can't display JPEGs, then it's probably easiest to modify CustomBanner
148
   *   and override scanDisplay on the modified class.
149
   *
150
   *  By default, we simply check that the interpreter we're running on
151
   *  can display banners.
152
   */
153
  canDisplay = (systemInfo(SysInfoBanners))  
154
  
155
  shouldDisplay = (canDisplay && isActive)
156
   
157
   /*
158
    *  The standard use of initBannerWindow is first to ensure that any
159
    *  banner windows whose existence we presuppose have themselves been
160
    *  initialized, and then to set up our own window on screen.
161
    *  This function should be used for initializing banner window *layout*,
162
    *  not content.
163
    */
164
   
165
   
166
  initBannerWindow()
167
  {
168
     /*
169
      *  If we shouldn't display on this class of interpreter, don't
170
      *  initialize us.
171
      */
172
     if(!shouldDisplay)
173
       return nil;
174
       
175
     /*
176
      *  If we've already been initialized, there's nothing left to do.
177
      */
178
        
179
     if(inited_)
180
       return true;  
181
       
182
     /*  
183
      *  Initialize all the bannner windows on whose existence our own
184
      *  depends. If one of them can't be initialized, neither can we,
185
      *  in which case return nil to show that our initialization failed. 
186
      *  If, however, the parent or sibling banner window we want initialized
187
      *  before us is not a CustomBannerWindow, then its initBannerWindow()
188
      *  won't have a return value, in which case we ignore the fact that
189
      *  it returns nil 
190
      */
191
       
192
     foreach(local ban in initBeforeMe)
193
       if(ban && !ban.initBannerWindow() && ban.ofKind(CustomBannerWindow))
194
         return nil;
195
        
196
     /*  
197
      * Create my banner window on screen; if this fails return nil
198
      * to indicate that the window could not be created
199
      */       
200
      
201
      return (inited_ = initBannerLayout());
202
203
  }
204
  
205
  /*
206
   *  Initialize my onscreen layout, normally through a call to showBanner(),
207
   *  whose return value this method should return, e.g.:
208
   *
209
   *     initBannerLayout()
210
   *     {
211
   *         return showBanner(nil, BannerAfter,  statuslineBanner,
212
   *           BannerTypeText, BannerAlignTop, 1, BannerSizeAbsolute,
213
   *           BannerStyleBorder); 
214
   *     }
215
   *
216
   *    By default we simply call initBannerLayout() using our bannerArgs.
217
   */
218
  
219
  initBannerLayout()
220
  {
221
     return showBanner(bannerArgs...);
222
  }
223
  
224
   
225
  
226
  /* 
227
   * The list of args used to define our screen layout, as they would be passed
228
   * to showBanner. This is used both by initBannerLayout and initBeforeMe.
229
   *
230
   *  The args should be listed in the form
231
   *
232
   *  bannerArgs = [parent, where, other, windowType, align, size, sizeUnits, styleFlags]
233
   *
234
   *  e.g.
235
   *    bannerArgs = [nil, BannerAfter,  statuslineBanner,
236
   *           BannerTypeText, BannerAlignTop, 1, BannerSizeAbsolute,
237
   *           BannerStyleBorder]
238
   *   
239
   */
240
    
241
  bannerArgs = nil
242
  
243
  /*
244
   *  The current contents to be displayed in this window, which could be 
245
   *  a string of text, or the HTML string to display a picture.
246
   *
247
   *  currentContents can be overridden to hold the initial contents
248
   *  we want this banner to display, but it should not otherwise be
249
   *  directly written to in game code. To display new contents in the
250
   *  banner, use updateContents() instead.  
251
   */
252
  currentContents = ''
253
  
254
  /*
255
   *  Is this banner currently active? Set to nil if you don't want to this
256
   *  CustomBannerWindow to be active at startup; thereafter use the deactivate()
257
   *  and activate() methods   
258
   */
259
   
260
  isActive = true
261
  
262
  /*
263
   *  deactivate a currently active banner; this removes it from the screen
264
   *  and prevents writing anything further to it. Be careful to respect the
265
   *  dependency order of banner windows when activating and deactivating
266
   *
267
   *  The argument is optional. If it is the constant true then the currentContents
268
   *  will be set to an empty string (''). If it is a string, then the currentContents
269
   *  will be set to that string (ready to be displayed when the banner is reactivated).
270
   */
271
   
272
  deactivate([args])  
273
  {     
274
     removeBanner();
275
     isActive = nil;
276
     
277
     if(args.length > 0)
278
     {
279
        local arg = args[1];
280
        switch(dataType(arg))
281
        {
282
           case TypeTrue:
283
             currentContents = '';
284
             break;
285
           case TypeSString:
286
             currentContents = arg;
287
             break;
288
        }
289
     }
290
     
291
  }
292
   
293
  /* 
294
   *  Activate a currently inactive banner; this restores it to the screen. 
295
   *  The argument is optional; if present and true then activate(true)
296
   *  displays the current contents of the banner window after activating it.
297
   *  If the first argument is a string then the string is displayed in the banner.   
298
   */ 
299
  activate([args])
300
  {
301
      if(isActive)
302
         return;
303
         
304
      isActive = true;
305
      initBannerWindow();
306
      
307
      if(args.length() > 0 && args[1] != nil)
308
      {
309
         if(dataType(args[1]) == TypeSString)
310
            updateContents(args...);
311
         else  
312
            showCurrentContents();
313
      }
314
  }
315
   
316
  removeBanner()
317
  {
318
    /*
319
     *  If I'm removed I can't be inited_ any more, and I'll need to be regarded
320
     *  as not inited_ in the event of being redisplayed in the future.
321
     */
322
  
323
    inited_ = nil;
324
    
325
    inherited;
326
  } 
327
   
328
   
329
  /*
330
   * Set this flag to true to clear the contents of the window before displaying
331
   * the new contents, e.g. to display a new picture that replaces the old one.
332
   */
333
  clearBeforeUpdate =  true
334
  
335
  /* 
336
   *  Set this to true to have this banner size to contents each time its
337
   *  contents are displayed. Note that not all interpreters support the size to
338
   *  contents so you should still set an appropriate initial size, and, where
339
   *  appropriate, call setSize() with the isAdvisory flag set.
340
   */
341
    
342
  autoSize = nil
343
  
344
  
345
  /*
346
   *  Update the contents of this banner window. This is the method to
347
   *  call to change what a banner displays.   
348
   *
349
   *  The second argument is optional. If present it overrides the 
350
   *  setting of clearBeforeUpdate: updateContents(cont, true) will
351
   *  clear the banner before the update, whereas updateContents(cont, nil)
352
   *  will not, whatever the value of clearBeforeUpdate. 
353
   */
354
   
355
  updateContents(cont, [args])
356
  {
357
    /*
358
     *  Update our current contents. Note that this takes place even if
359
     *  shouldDisplay is nil, so that if, for example, we are updated on
360
     *  a text-only interpreter on which this banner is not displayed, 
361
     *  and the game is saved there and subsequently restored on a full HTML 
362
     *  interpreter in which we are displayed, the HTML interpreter will know 
363
     *  what contents it needs to display in us.
364
     */
365
    currentContents = cont;
366
    
367
    showCurrentContents(args...);
368
  }
369
  
370
  /* Show the current contents of this banner window */  
371
  
372
  showCurrentContents([args])
373
  { 
374
    local clr;
375
    if(args.length > 0)
376
      clr = (args[1] != nil);
377
    else
378
      clr = clearBeforeUpdate;
379
     
380
    if(clr)    
381
       clearWindow();
382
          
383
     writeToBanner(currentContents);      
384
     
385
     if(autoSize)
386
       sizeToContents();
387
  }
388
  
389
  
390
   /*  This is called on each CustomBannerWindow after a Restore. */   
391
   
392
  restoreBannerDisplay()
393
  {
394
        /* 
395
         * It's possible a game was saved in a text-mode terp and
396
         * restored in an HTML one. In which case we need to initialize
397
         * this banner before attempting to display anything
398
         */
399
         
400
         if(shouldDisplay && handle_ == nil)
401
         {
402
            if(!initBannerWindow())
403
              return;
404
         }
405
                  
406
        
407
        /* redisplay my contents after a restore */        
408
        showCurrentContents(); 
409
  }  
410
  
411
  /*
412
   *  Alternatively we might have been saved in a terp that does
413
   *  use this banner and restored in one that doesn't, in which
414
   *  case we should remove ourselves. This is called on each BannerWindow
415
   *  after a restore, but before bannerTracker.restoreDisplayState().
416
   */
417
     
418
  restoreRemove()
419
  {
420
     if(!shouldDisplay)        
421
        removeBanner();     
422
  }
423
  
424
  
425
  /* show my initial contents on startup */
426
       
427
  initBannerDisplay()  {  showCurrentContents();   }
428
  
429
  /*
430
   *   We provide overrides for all the various banner manipulation methods
431
   *   that game code might call, in order to make it safe to call them even
432
   *   our shouldDisplay method returns nil and we don't - or shouldn't - exist.
433
   *   For each of these xxxYyy methods we provide an altXxxyyy method that is
434
   *   called when shouldDisplay is nil (e.g. because we're using a window to
435
   *   display graphics on an interpreter that doesn't have graphics capablities).
436
   *   By default these altXxxYyy methods do nothing, which in many cases will
437
   *   be fine, but if you do want something else to happen you can override
438
   *   the appropriate altXxxYyy method accordingly (e.g. to show a message in
439
   *   the main game window instead of this banner). This should make it easier
440
   *   to structure the rest of your game code without needing to worry about
441
   *   what happens on interpreters which don't display your banners.
442
   */
443
   
444
  clearWindow()
445
  {
446
    if(shouldDisplay)
447
      inherited();
448
    else
449
      altClearWindow();
450
  }
451
  
452
  altClearWindow() { }
453
    
454
  /* write to me, but only if I should display */ 
455
   
456
  writeToBanner(txt)
457
  {  
458
    if(shouldDisplay)
459
      inherited(txt);
460
    else
461
      altWriteToBanner(txt);
462
  }
463
  
464
  /*
465
   *  altWriteToBanner(txt) is called when our game code tries to display
466
   *  something in this banner, but our shouldDisplay method has ruled out
467
   *  displaying this banner. In this case we might want to write something
468
   *  to the main display instead. By default we do nothing here, but
469
   *  individual instances and/or subclasses can override this method as
470
   *  required.
471
   */
472
  
473
  altWriteToBanner(txt) { }
474
  
475
  
476
  /* 
477
   *  We don't provide alternative methods for the setSize and sizeToContents
478
   *  methods, since there would almost certainly be nothing for them to do.
479
   *  We simply do nothing if shouldDisplay is nil.
480
   */
481
  
482
  setSize(size, sizeUnits, isAdvisory)
483
  {
484
       if(shouldDisplay)
485
         inherited(size, sizeUnits, isAdvisory);
486
  }
487
     
488
  sizeToContents()
489
  {
490
        /* size our system-level window to our contents */
491
       if(shouldDisplay)  
492
         bannerSizeToContents(handle_);
493
  }
494
495
  captureOutput(func)
496
  {
497
     if(shouldDisplay)
498
       inherited(func);
499
     else
500
       altCaptureOutput(func);  
501
  }
502
  
503
  /* Simply execute the callback without changing the output stream */
504
  
505
  altCaptureOutput(func) { (func)(); }
506
           
507
  setOutputStream()
508
     {
509
        if(shouldDisplay)
510
           /* set my stream as the default */
511
           return outputManager.setOutputStream(outputStream_);
512
        else
513
           return altSetOutputStream();          
514
     }
515
     
516
  /* 
517
   *  Our caller, or rather our caller's caller, will expect us to return
518
   *  the current output stream, which means we must be sure to do this
519
   *  whatever else we do.  
520
   */
521
         
522
  altSetOutputStream() { return outputManager.curOutputStream; }         
523
  
524
  flushBanner() 
525
  { 
526
    if(shouldDisplay)
527
      inherited();
528
    else  
529
      altFlushBanner();
530
  }
531
         
532
  altFlushBanner() { }       
533
           
534
  setTextColor(fg, bg)
535
  {
536
    if(shouldDisplay)
537
      inherited(fg, bg);
538
    else
539
      altSetTextColor(fg, bg);
540
  }         
541
          
542
  altSetTextColor(fg, bg) { }
543
  
544
  setScreenColor(color)
545
  {
546
    if(shouldDisplay)
547
      inherited(color);
548
    else
549
      altSetScreenColor(color);
550
  }
551
  
552
  altSetScreenColor(color) { }
553
  
554
  cursorTo(row, col)
555
  {
556
    if(shouldDisplay)
557
      inherited(row, col);
558
    else
559
      altCursorTo(row, col);
560
  }             
561
           
562
  /* 
563
   *  If this banner isn't displaying we can't do anything directly comparable
564
   *  to setting the cursot to a particular column and row in it, but we might
565
   *  want to do something else instead, like inserting so many blank lines in 
566
   *  the main window.            
567
   */       
568
  altCursorTo(row, col) { }         
569
;
570
571
572
573
 /*
574
  *  Initialize or reinitialize what all CustomBanners display at startup or
575
  *  after an UNDO
576
  */
577
578
customBannerInit: InitObject, PostUndoObject
579
  execBeforeMe =  [bannerInit]
580
  
581
  execute()
582
  {
583
     /* first ensure that all banner windows that need to exist do exist */
584
     
585
//     forEachInstance(CustomBannerWindow, new function(win) {
586
//        if(win.shouldDisplay && win.handle_ == nil)
587
//          win.initBannerWindow(); 
588
//     } );
589
  
590
     /* then show the current contents of every active banner */
591
  
592
     forEachInstance(CustomBannerWindow, {win: win.showCurrentContents() } );
593
  }
594
;
595
596
  /*
597
   *  Reinitialize what all the CustomBanners display on restoring. This requires
598
   *  a different procedure since we can't be sure that we're being restored on
599
   *  the same class of interpreter as we were saved on.
600
   */
601
602
customBannerRestore: PostRestoreObject
603
   execBeforeMe = [bannerTracker] 
604
605
   execute()
606
   {
607
   
608
     /*
609
      *  If we save in one terp, restore in the second terp, save in the second
610
      *  terp, then restore in the first terp, when different rules apply about
611
      *  displaying banners in the two terps, then windows removed in the second
612
      *  terp could still be marked as inited_ in the restore file that comes
613
      *  back to the first terp. To get round this, on restoration we ensure
614
      *  that each CustomBanner's inited_ property in fact corresponds to whether
615
      *  it has an active handle_, otherwise the attempt to reinitialize missing
616
      *  banners might fail.
617
      */
618
     forEachInstance(CustomBannerWindow, {win: win.inited_ = (win.handle_ != nil) } );
619
       
620
     forEachInstance(CustomBannerWindow, {win: win.restoreBannerDisplay() } );
621
   }
622
623
;
624
625
customBannerRestoreRemove: PostRestoreObject
626
  execAfterMe = [bannerTracker]
627
628
  execute()
629
  {
630
      forEachInstance(CustomBannerWindow, {win: win.restoreRemove() } );
631
  }
632
;
633
634
 /*
635
  *  If we display a menu then we need to remove any active banners from the
636
  *  screen before the menu displays and restore them to the screen on exiting
637
  *  from the menu
638
  */
639
modify MenuItem
640
  display()
641
  {
642
    /*
643
     *  First we store a list of all the banners that are currently
644
     *  displaying
645
     */
646
    local vec = new Vector(10);
647
     
648
    forEachInstance(CustomBannerWindow, new function(ban) {
649
        if(ban.shouldDisplay)
650
          vec.append(ban);  } );    
651
          
652
    /* deactive all active banners */      
653
          
654
    foreach(local ban in vec)
655
       ban.deactivate();
656
        
657
    try
658
    {
659
       /* carry out the inherited menu display */
660
       inherited();
661
    }  
662
      
663
   /* 
664
    *  Restore all the banners in our list of banners that were previously
665
    *  displayed. To ensure that they are activated in the right order
666
    *  we make what may be several passes through the list. On each pass
667
    *  we activate only those banners that don't depend on any inactive
668
    *  banners for their activation. Each time we activate a banner, we
669
    *  remove it from the list. On the next pass through the list any
670
    *  banners that depended on banners we have just activated may now themselves
671
    *  be activated, so we can carry on until every banner has been activated
672
    *  and removed from the list.  
673
    */  
674
   finally
675
   {
676
     while(vec.length())
677
     {
678
       local bannerRemoved = nil;
679
       
680
       foreach(local ban in vec)
681
       {
682
683
       
684
         if(ban.bannerArgs[1] != nil && ban.bannerArgs[1].handle_ == nil)
685
            continue;
686
           
687
         if(ban.bannerArgs[3] != nil && ban.bannerArgs[3].handle_ == nil)
688
            continue; 
689
             
690
         ban.activate(true);
691
         vec.removeElement(ban);
692
         bannerRemoved = true;      
693
       }  
694
       
695
       /*
696
        *  If we didn't remove any banners on this pass through, we're
697
        *  potentially in an infinite loop, so we'd better break out
698
        *  of it.
699
        */
700
       if(!bannerRemoved)
701
         break;
702
     }
703
      
704
    }            
705
  }
706
; 
707