banner.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library: banner manager
  *   
  *   This module defines the banner manager, which provides high-level
  *   services to create and manipulate banner windows.
  *   
  *   A "banner" is an independent window shown within the interpreter's
  *   main application display frame (which might be the entire screen on a
  *   character-mode terminal, or could be a window in a GUI system).  The
  *   game can control the creation and destruction of banner windows, and
  *   can control their placement and size.
  *   
  *   This implementation is based in part on Steve Breslin's banner
  *   manager, used by permission.  
  */
 
 #include "adv3.h"
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A BannerWindow corresponds to an on-screen banner.  For each banner
  *   window a game wants to display, the game must create an object of this
  *   class.
  *   
  *   Note that merely creating a BannerWindow object doesn't actually
  *   display a banner window.  Once a BannerWindow is created, the game
  *   must call the object's showBanner() method to create the on-screen
  *   window for the banner.
  *   
  *   BannerWindow instances are intended to be persistent (not transient).
  *   The banner manager keeps track of each banner window that's actually
  *   being displayed separately via an internal transient object; the game
  *   doesn't need to worry about these tracking objects, since the banner
  *   manager automatically handles them.  
  */
 class BannerWindow: object
     /*
      *   Construct the object.
      *   
      *   'id' is a globally unique identifying string for the banner.  When
      *   we dynamically create a banner object, we have to provide a unique
      *   identifying string, so that we can correlate transient on-screen
      *   banners with the banners in a saved state when restoring the saved
      *   state.
      *   
      *   Note that no ID string is needed for BannerWindow objects defined
      *   statically at compile-time, because the object itself ('self') is
      *   a suitably unique and stable identifier.  
      */
     construct(id)
     {
         /* remember my unique identifier */
         id_ = id;
     }
 
     /*
      *   Show the banner.  The game should call this method when it first
      *   wants to display the banner.
      *   
      *   'parent' is the parent banner; this is an existing BannerWindow
      *   object.  If 'parent' is nil, then the parent is the main game
      *   text window.  The new window's display space is obtained by
      *   carving space out of the parent's area, according to the
      *   alignment and size values specified.
      *   
      *   'where' and 'other' give the position of the banner among the
      *   children of the given parent.  'where' is one of the constants
      *   BannerFirst, BannerLast, BannerBefore, or BannerAfter.  If
      *   'where' is BannerBefore or BannerAfter, 'other' gives the
      *   BannerWindow object to be used as the reference point in the
      *   parent's child list; 'other' is ignored in other cases.  Note
      *   that 'other' must always be another child of the same parent; if
      *   it's not, then we act as though 'where' were given as BannerLast.
      *   
      *   'windowType' is a BannerTypeXxx constant giving the new window's
      *   type.
      *   
      *   'align' is a BannerAlignXxx constant giving the alignment of the
      *   new window.  'size' is an integer giving the size of the banner,
      *   in units specified by 'sizeUnits', which is a BannerSizeXxx
      *   constant.  If 'size' is nil, it indicates that the caller doesn't
      *   care about the size, usually because the caller will be resizing
      *   the banner soon anyway; the banner will initially have zero size
      *   in this case if we create a new window, or will retain the
      *   existing size if there's already a system window.
      *   
      *   'styleFlags' is a combination of BannerStyleXxx constants
      *   (combined with the bitwise OR operator, '|'), giving the requested
      *   display style of the new banner window.
      *   
      *   Note that if we already have a system banner window, and the
      *   existing banner window has the same characteristics as the new
      *   creation parameters, we'll simply re-use the existing window
      *   rather than closing and re-creating it; this reduces unnecessary
      *   redrawing in cases where the window isn't changing.  If the caller
      *   explicitly wants to create a new window even if we already have a
      *   window, the caller should simply call removeBanner() before
      *   calling this routine.  
      */
     showBanner(parent, where, other, windowType,
                align, size, sizeUnits, styleFlags)
     {
         local parentID;
         local otherID;
 
         /* note the ID's of the parent window and the insertion point */
         parentID = (parent != nil ? parent.getBannerID() : nil);
         otherID = (other != nil ? other.getBannerID() : nil);
 
         /* 
          *   if we have an 'other' specified, its parent must match our
          *   proposed parent; otherwise, ignore 'other' and insert at the
          *   end of the parent list 
          */
         if (other != nil && other.parentID_ != parentID)
         {
             other = nil;
             where = BannerLast;
         }
 
         /* if we already have an existing banner window, check for a match */
         if (handle_ != nil)
         {
             local t;
             local match;
 
             /* presume we won't find an exact match */
             match = nil;
 
             /* we already have a window - get the UI tracker object */
             if ((t = bannerUITracker.getTracker(self)) != nil)
             {
                 /* check the placement, window type, alignment, and style */
                 match = (t.windowType_ == windowType
                          && t.parentID_ == parentID
                          && t.align_ == align
                          && t.styleFlags_ == styleFlags
                          && bannerUITracker.orderMatches(t, where, otherID));
             }
 
             /* 
              *   if it doesn't match the existing window, close it, so that
              *   we will open a brand new window with the new
              *   characteristics 
              */
             if (!match)
                 removeBanner();
         }
 
         /* if the system-level banner doesn't already exist, create it */
         if (handle_ == nil)
         {
             /* create my system-level banner window */
             if (!createSystemBanner(parent, where, other, windowType, align,
                                     size, sizeUnits, styleFlags))
             {
                 /* we couldn't create the system banner - give up */
                 return nil;
             }
 
             /* create our output stream */
             createOutputStream();
 
             /* add myself to the UI tracker's active banner list */
             bannerUITracker.addBanner(handle_, outputStream_, getBannerID(),
                                       parentID, where, other, windowType,
                                       align, styleFlags);
         }
         else
         {
             /* 
              *   Our system-level window already exists, so we don't need
              *   to create a new one.  However, our size could be
              *   different, so explicitly set the requested size if it
              *   doesn't match our recorded size.  If the size is given as
              *   nil, leave the size as it is; a nil size indicates that
              *   the caller doesn't care about the size (probably because
              *   the caller is going to change the size shortly anyway),
              *   so we can avoid unnecessary redrawing by leaving the size
              *   as it is for now.  
              */
             if (size != nil && (size != size_ || sizeUnits != sizeUnits_))
                 bannerSetSize(handle_, size, sizeUnits, nil);
         }
 
         /* 
          *   remember the creation parameters, so that we can re-create the
          *   banner with the same characteristics in the future if we
          *   should need to restore the banner from a saved position 
          */
         parentID_ = parentID;
         windowType_ = windowType;
         align_ = align;
         size_ = size;
         sizeUnits_ = sizeUnits;
         styleFlags_ = styleFlags;
 
         /* 
          *   Add myself to the persistent banner tracker's active list.  Do
          *   this even if we already had a system handle, since we might be
          *   initializing the window as part of a persistent restore
          *   operation, in which case the persistent tracking object might
          *   not yet exist.  (This seems backwards: if we're restoring a
          *   persistent state, surely the persistent tracker would already
          *   exist.  In fact, the case we're really handling is where the
          *   window is open in the transient UI, because it was already
          *   open in the ongoing session; but the persistent state we're
          *   restoring doesn't include the window.  This is most likely to
          *   occur after a RESTART, since we could have a window that is
          *   always opened immediately at start-up and thus will be in the
          *   transient state up to and through the RESTART, but is only
          *   created as part of the initialization process.)  
          */
         bannerTracker.addBanner(self, parent, where, other);
 
         /* indicate success */
         return true;
     }
 
     /*
      *   Remove the banner.  This removes the banner's on-screen window.
      *   The BannerWindow object itself remains valid, but after this
      *   method returns, the BannerWindow no longer has an associated
      *   display window.
      *   
      *   Note that any child banners of ours will become undisplayable
      *   after we're gone.  A child banner depends upon its parent to
      *   obtain display space, so once the parent is gone, its children no
      *   longer have any way to obtain any display space.  Our children
      *   remain valid objects even after we're closed, but they won't be
      *   visible on the display.    
      */
     removeBanner()
     {
         /* if I don't have a system-level handle, there's nothing to do */
         if (handle_ == nil)
             return;
 
         /* remove my system-level banner window */
         bannerDelete(handle_);
 
         /* our system-level window is gone, so forget its handle */
         handle_ = nil;
 
         /* we only need an output stream when we're active */
         outputStream_ = nil;
 
         /* remove myself from the UI trackers's active list */
         bannerUITracker.removeBanner(getBannerID());
 
         /* remove myself from the persistent banner tracker's active list */
         bannerTracker.removeBanner(self);
     }
 
     /* write the given text to the banner */
     writeToBanner(txt)
     {
         /* write the text to our underlying output stream */
         outputStream_.writeToStream(txt);
     }
 
     /* 
      *   Invoke the given callback function, setting the default output
      *   stream to the banner's output stream for the duration of the
      *   call.  This allows invoking any code that writes to the current
      *   default output stream and displaying the result in the banner.  
      */
     captureOutput(func)
     {
         local oldStr;
         
         /* make my output stream the global default */
         oldStr = outputManager.setOutputStream(outputStream_);
 
         /* make sure we restore the default output stream on the way out */
         try
         {
             /* invoke the callback function */
             (func)();
         }
         finally
         {
             /* restore the original default output stream */
             outputManager.setOutputStream(oldStr);
         }
     }
 
     /* 
      *   Make my output stream the default in the output manager.  Returns
      *   the previous default output stream; the caller can note the return
      *   value and use it later to restore the original output stream via a
      *   call to outputManager.setOutputStream(), if desired.  
      */
     setOutputStream()
     {
         /* set my stream as the default */
         return outputManager.setOutputStream(outputStream_);
     }
 
     /* flush any pending output to the banner */
     flushBanner() { bannerFlush(handle_); }
 
     /*
      *   Set the banner window to a specific size.  'size' is the new
      *   size, in units given by 'sizeUnits', which is a BannerSizeXxx
      *   constant.
      *   
      *   'isAdvisory' is true or nil; if true, it indicates that the size
      *   setting is purely advisory, and that a sizeToContents() call will
      *   eventually follow to set the actual size.  When 'isAdvisory is
      *   true, the interpreter is free to ignore the request if
      *   sizeToContents() 
      */
     setSize(size, sizeUnits, isAdvisory)
     {
         /* set the underlying system window size */
         bannerSetSize(handle_, size, sizeUnits, isAdvisory);
 
         /* 
          *   remember my new size in case we have to re-create the banner
          *   from a saved state 
          */
         size_ = size;
         sizeUnits_ = sizeUnits;
     }
 
     /*
      *   Size the banner to its current contents.  Note that some systems
      *   do not support this operation, so callers should always make an
      *   advisory call to setSize() first to set a size based on the
      *   expected content size.  
      */
     sizeToContents()
     {
         /* size our system-level window to our contents */
         bannerSizeToContents(handle_);
     }
 
     /*
      *   Clear my banner window.  This clears out all of the contents of
      *   our on-screen display area.  
      */
     clearWindow()
     {
         /* clear our system-level window */
         bannerClear(handle_);
     }
 
     /* set the text color in the banner */
     setTextColor(fg, bg) { bannerSetTextColor(handle_, fg, bg); }
 
     /* set the screen color in the banner window */
     setScreenColor(color) { bannerSetScreenColor(handle_, color); }
 
     /* 
      *   Move the cursor to the given row/column position.  This can only
      *   be used with text-grid banners; for ordinary text banners, this
      *   has no effect. 
      */
     cursorTo(row, col) { bannerGoTo(handle_, row, col); }
 
     /*
      *   Get the banner identifier.  If our 'id_' property is set to nil,
      *   we'll assume that we're a statically-defined object, in which case
      *   'self' is a suitable identifier.  Otherwise, we'll return the
      *   identifier string. 
      */
     getBannerID() { return id_ != nil ? id_ : self; }
 
     /*
      *   Restore this banner.  This is called after a RESTORE or UNDO
      *   operation that finds that this banner was being displayed at the
      *   time the state was saved but is not currently displayed in the
      *   active UI.  We'll show the banner using the characteristics saved
      *   persistently.
      */
     showForRestore(parent, where, other)
     {
         /* show myself, using my saved characteristics */
         showBanner(parent, where, other, windowType_, align_,
                    size_, sizeUnits_, styleFlags_);
 
         /* update my contents */
         updateForRestore();
     }
 
     /*
      *   Create our output stream.  We'll create a BannerOutputStream and
      *   set it up with our default output filters.  Subclasses can
      *   override this as needed to customize the output stream. 
      */
     createOutputStream()
     {
         /* create a banner output stream */
         outputStream_ = new transient BannerOutputStream(handle_);
 
         /* set up the default filters */
         outputStream_.addOutputFilter(typographicalOutputFilter);
         outputStream_.addOutputFilter(new transient ParagraphManager());
         outputStream_.addOutputFilter(styleTagFilter);
         outputStream_.addOutputFilter(langMessageBuilder);
     }
 
     /*
      *   Create the system-level banner window.  This can be customized as
      *   needed, although this default implementation should be suitable
      *   for most instances.
      *   
      *   Returns true if we are successful in creating the system window,
      *   nil if we fail.  
      */
     createSystemBanner(parent, where, other, windowType, align,
                        size, sizeUnits, styleFlags)
     {
         /* create the system-level window */
         handle_ = bannerCreate(parent != nil ? parent.handle_ : nil,
                                where, other != nil ? other.handle_ : nil,
                                windowType, align, size, sizeUnits,
                                styleFlags);
 
         /* if we got a valid handle, we succeeded */
         return (handle_ != nil);
     }
 
     /*
      *   Update my contents after being restored.  By default, this does
      *   nothing; instances might want to override this to refresh the
      *   contents of the banner if the banner is normally updated only in
      *   response to specific events.  Note that it's not necessary to do
      *   anything here if the banner will soon be updated automatically as
      *   part of normal processing; for example, the status line banner is
      *   updated at each new command line via a prompt-daemon, so there's
      *   no need for the status line banner to do anything here.  
      */
     updateForRestore()
     {
         /* do nothing by default; subclasses can override as needed */
     }
 
     /*
      *   Initialize the banner window.  This is called during
      *   initialization (when first starting the game, or when resetting
      *   with RESTART).  If the banner is to be displayed from the start of
      *   the game, this can set up the on-screen display.
      *   
      *   Note that we might already have an on-screen handle when this is
      *   called.  This indicates that we're restarting an ongoing session,
      *   and that this banner already existed in the session before the
      *   RESTART operation.  If desired, we can attach ourselves to the
      *   existing on-screen banner, avoiding the redrawing that would occur
      *   if we created a new window.
      *   
      *   If this window depends upon another window for its layout order
      *   placement (i.e., we'll call showBanner() with another BannerWindow
      *   given as the 'other' parameter), then this routine should call the
      *   other window's initBannerWindow() method before creating its own
      *   window, to ensure that the other window has a system window and
      *   thus will be meaningful to establish the layout order.
      *   
      *   Overriding implementations should check the 'inited_' property.
      *   If this property is true, then it can be assumed that we've
      *   already been initialized and don't require further initialization.
      *   This routine can be called multiple times because dependent
      *   windows might call us directly, before we're called for our
      *   regular initialization.  
      */
     initBannerWindow()
     {
         /* by default, simply note that we've been initialized */
         inited_ = true;
     }
 
     /* flag: this banner has been initialized with initBannerWindow() */
     inited_ = nil
 
     /* 
      *   The creator-assigned ID string to identify the banner
      *   persistently.  This is only needed for banners created
      *   dynamically; for BannerWindow objects defined statically at
      *   compile time, simply leave this value as nil, and we'll use the
      *   object itself as the identifier.  
      */
     id_ = nil
 
     /* the handle to my system-level banner window */
     handle_ = nil
 
     /*
      *   My output stream - this is a transient OutputStream instance.
      *   We'll automatically create an output stream when we show the
      *   banner.  
      */
     outputStream_ = nil
 
     /* 
      *   Creation parameters.  We store these when we create the banner,
      *   and update them as needed when the banner's display attributes
      *   are changed.  
      */
     parentID_ = nil
     windowType_ = nil
     align_ = nil
     size_ = nil
     sizeUnits_ = nil
     styleFlags_ = nil
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Banner Output Stream.  This is a specialization of OutputStream that
  *   writes to a banner window.  
  */
 class BannerOutputStream: OutputStream
     /* construct */
     construct(handle)
     {
         /* inherit base class constructor */
         inherited();
         
         /* remember my banner window handle */
         handle_ = handle;
     }
 
     /* execute preinitialization */
     execute()
     {
         /*
          *   We shouldn't need to do anything during pre-initialization,
          *   since we should always be constructed dynamically by a
          *   BannerWindow.  Don't even inherit the base class
          *   initialization, since it could clear out state that we want to
          *   keep through a restart, restore, etc.  
          */
     }
 
     /* write text from the stream to the interpreter I/O system */
     writeFromStream(txt)
     {
         /* write the text to the underlying system banner window */
         bannerSay(handle_, txt);
     }
 
     /* our system-level banner window handle */
     handle_ = nil
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   The banner UI tracker.  This object keeps track of the current user
  *   interface display state; this object is transient because the
  *   interpreter's user interface is not part of the persistence
  *   mechanism.  
  */
 transient bannerUITracker: object
     /* add a banner to the active display list */
     addBanner(handle, ostr, id, parentID, where, other,
               windowType, align, styleFlags)
     {
         local uiWin;
         local parIdx;
         local idx;
 
         /* create a transient BannerUIWindow object to track the banner */
         uiWin = new transient BannerUIWindow(handle, ostr, id, parentID,
                                              windowType, align, styleFlags);
 
         /* 
          *   Find the parent in the list.  If there's no parent, the
          *   parent is the main window; consider it to be at imaginary
          *   index zero in the list. 
          */
         parIdx = (parentID == nil
                   ? 0 : activeBanners_.indexWhich({x: x.id_ == parentID}));
 
         /* insert the banner at the proper point in our list */
         switch(where)
         {
         case BannerFirst:
             /* 
              *   insert as the first child of the parent - put it
              *   immediately after the parent in the list 
              */
             activeBanners_.insertAt(parIdx + 1, uiWin);
             break;
 
         case BannerLast:
         ins_last:
             /* 
              *   Insert as the last child of the parent: insert
              *   immediately after the last window that descends from the
              *   parent.  
              */
             activeBanners_.insertAt(skipDescendants(parIdx), uiWin);
             break;
 
         case BannerBefore:
         case BannerAfter:
             /* find the reference point ID in our list */
             idx = activeBanners_.indexWhich(
                 {x: x.id_ == other.getBannerID()});
 
             /* 
              *   if we didn't find the reference point, or the reference
              *   point item doesn't have the same parent as the new item,
              *   then ignore the reference point and instead insert at the
              *   end of the parent's child list 
              */
             if (idx == nil || activeBanners_[idx].parentID_ != parentID)
                 goto ins_last;
 
             /* 
              *   if inserting after, skip the reference item and all
              *   of its descendants 
              */
             if (where == BannerAfter)
                 idx = skipDescendants(idx);
 
             /* insert at the position we found */
             activeBanners_.insertAt(idx, uiWin);
             break;
         }
     }
 
     /*
      *   Given an index in our list of active windows, skip the given item
      *   and all items whose windows are descended from this window.
      *   We'll leave the index positioned on the next entry in the list
      *   that isn't a descendant of the window at the given index.  Note
      *   that this skips not only children but grandchildren (and so on)
      *   as well.  
      */
     skipDescendants(idx)
     {
         local parentID;
 
         /* 
          *   if the index is zero, it's the main window; all windows are
          *   children of the root window, so return the next index after
          *   the last item 
          */
         if (idx == 0)
             return activeBanners_.length() + 1;
 
         /* note ID of the parent item */
         parentID = activeBanners_[idx].id_;
         
         /* skip the parent item */
         ++idx;
 
         /* keep going as long as we see children of the parent */
         while (idx <= activeBanners_.length()
                && activeBanners_[idx].parentID_ == parentID)
         {
             /* 
              *   This is a child of the given parent, so we must skip it;
              *   we must also skip its descendants, since they're all
              *   indirectly descendants of the original parent.  So,
              *   simply skip this item and its descendants with a
              *   recursive call to this routine.
              */
             idx = skipDescendants(idx);
         }
 
         /* return the new index */
         return idx;
     }
 
     /* remove a banner from the active display list */
     removeBanner(id)
     {
         local idx;
 
         /* find the entry with the given ID, and remove it */
         if ((idx = activeBanners_.indexWhich({x: x.id_ == id})) != nil)
         {
             local lastIdx;
             
             /* 
              *   After removing an item, its children are no longer
              *   displayable, because a child obtains display space from
              *   its parent.  So, we must remove any children of this item
              *   at the same time we remove the item itself.  Find the
              *   index of the next item after all of our descendants, so
              *   that we can remove the item and its children all at once.
              *   An item and its descendants are always contiguous in our
              *   list, since we store children immediately after their
              *   parents, so we can simply remove the range of items from
              *   the specified item to its last descendant.
              *   
              *   Note that skipDescendants() returns the index of the
              *   first item that is NOT a descendant; so, decrement the
              *   result so that we end up with the index of the last
              *   descendant.  
              */
             lastIdx = skipDescendants(idx) - 1;
 
             /* remove the item and all of its children */
             activeBanners_.removeRange(idx, lastIdx);
         }
     }
 
     /* get the BannerUIWindow tracker object for a given BannerWindow */
     getTracker(win)
     {
         local id;
 
         /* get the window's ID */
         id = win.getBannerID();
         
         /* return the tracker with the same ID as the given BannerWindow */
         return activeBanners_.valWhich({x: x.id_ == id});
     }
 
     /* check a BannerUIWindow to see if it matches the given layout order */
     orderMatches(uiWin, where, otherID)
     {
         local idx;
         local otherIdx;
         local parentID;
         local parIdx;
 
         /* get the list index of the given window */
         idx = activeBanners_.indexOf(uiWin);
 
         /* get the list index of the reference point window */
         otherIdx = (otherID != nil
                     ? activeBanners_.indexWhich({x: x.id_ == otherID}) : nil);
 
         /* 
          *   find the parent item (using imaginary index zero for the
          *   root, which we can think of as being just before the first
          *   item in the list)
          */
         parentID = uiWin.parentID_;
         parIdx = (parentID == nil
                   ? 0 : activeBanners_.indexWhich({x: x.id_ == parentID}));
 
         /* 
          *   if 'other' is specified, it has to have our same parent; if
          *   it has a different parent, it's not a match 
          */
         if (otherID != nil && parentID != activeBanners_[otherIdx].parentID_)
             return nil;
 
         /* 
          *   if there's no such window in the list, it can't match the
          *   given placement no matter what the given placement is, as it
          *   has no placement 
          */
         if (idx == nil)
             return nil;
         
         /* check the requested layout order */
         switch (where)
         {
         case BannerFirst:
             /* make sure it's immediately after the parent */
             return idx == parIdx + 1;
 
         case BannerLast:
             /* 
              *   Make sure it's the last child of the parent.  To do this,
              *   make sure that the next item after this item's last
              *   descendant is the same as the next item after the
              *   parent's last descendant. 
              */
             return skipDescendants(idx) == skipDescendants(parIdx);
 
         case BannerBefore:
             /* 
              *   we want this item to come before 'other', so make sure
              *   the next item after all of this item's descendants is
              *   'other' 
              */
             return skipDescendants(idx) == otherIdx;
 
         case BannerAfter:
             /* 
              *   we want this item to come just after 'other', so make
              *   sure that the next item after all of the descendants of
              *   'other' is this item 
              */
             return skipDescendants(otherIdx) == idx;
 
         default:
             /* other layout orders are invalid */
             return nil;
         }
     }
 
     /*
      *   The vector of banners currently on the screen.  This is a list of
      *   transient BannerUIWindow objects, stored in the same order as the
      *   banner layout list.  
      */
     activeBanners_ = static new transient Vector(32)
 ;
 
 /*
  *   A BannerUIWindow object.  This keeps track of the transient UI state
  *   of a banner window while it appears on the screen.  We create only
  *   transient instances of this class, since it tracks what's actually
  *   displayed at any given time.  
  */
 class BannerUIWindow: object
     /* construct */
     construct(handle, ostr, id, parentID, windowType, align, styleFlags)
     {
         /* remember the banner's data */
         handle_ = handle;
         outputStream_ = ostr;
         id_ = id;
         parentID_ = parentID;
         windowType_ = windowType;
         align_ = align;
         styleFlags_ = styleFlags;
     }
 
     /* the system-level banner handle */
     handle_ = nil
 
     /* the banner's ID */
     id_ = nil
 
     /* the parent banner's ID (nil if this is a top-level banner) */
     parentID_ = nil
 
     /* 
      *   The banner's output stream.  Output streams are always transient,
      *   so hang on to each active banner's stream so that we can plug it
      *   back in on restore. 
      */
     outputStream_ = nil
 
     /* creation parameters of the banner */
     windowType_ = nil
     align_ = nil
     styleFlags_ = nil
 
     /* 
      *   Scratch-pad for our association to our BannerWindow object.  We
      *   only use this during the RESTORE process, to tie the transient
      *   object back to the proper persistent object. 
      */
     win_ = nil
 ;
 
 /*
  *   The persistent banner tracker.  This keeps track of the active banner
  *   windows persistently.  Whenever we save or restore the game's state,
  *   this object will be saved or restored along with the state.  When we
  *   restore a previously saved state, we can look at this object to
  *   determine which banners were active at the time the state was saved,
  *   and use this information to restore the same active banners in the
  *   user interface.
  *   
  *   This is a post-restore and post-undo object, so we're notified via our
  *   execute() method whenever we restore a saved state using RESTORE or
  *   UNDO.  When we restore a saved state, we'll restore the banner display
  *   conditions as they existed in the saved state.  
  */
 bannerTracker: PostRestoreObject, PostUndoObject
     /* add a banner to the active display list */
     addBanner(win, parent, where, other)
     {
         local parIdx;
         local otherIdx;
         
         /* 
          *   Don't add it if it's already in the list.  If we're restoring
          *   the banner from persistent state, it'll already be in the
          *   active list, since the active list is the set of windows
          *   we're restoring in the first place. 
          */
         if (activeBanners_.indexOf(win) != nil)
             return;
 
         /* find the parent among the existing windows */
         parIdx = (parent == nil ? 0 : activeBanners_.indexOf(parent));
 
         /* note the index of 'other' */
         otherIdx = (other == nil ? nil : activeBanners_.indexOf(other));
 
         /* insert the banner at the proper point in our list */
         switch(where)
         {
         case BannerFirst:
             /* insert immediately after the parent */
             activeBanners_.insertAt(parIdx + 1, win);
             break;
 
         case BannerLast:
         ins_last:
             /* insert after the parent's last descendant */
             activeBanners_.insertAt(skipDescendants(parIdx), win);
             break;
 
         case BannerBefore:
         case BannerAfter:
             /* 
              *   if we didn't find the reference point, insert at the end
              *   of the parent's child list 
              */
             if (otherIdx == nil)
                 goto ins_last;
 
             /* 
              *   if inserting after, skip the reference item and all of
              *   its descendants 
              */
             if (where == BannerAfter)
                 otherIdx = skipDescendants(otherIdx);
 
             /* insert at the position we found */
             activeBanners_.insertAt(otherIdx, win);
             break;
         }
     }
 
     /*
      *   Skip all descendants of the window at the given index. 
      */
     skipDescendants(idx)
     {
         local parentID;
 
         /* index zero is the root item, so skip the entire list */
         if (idx == 0)
             return activeBanners_.length() + 1;
         
         /* note the parent item */
         parentID = activeBanners_[idx].getBannerID();
 
         /* skip the parent item */
         ++idx;
 
         /* keep going as long as we see children of the parent */
         while (idx < activeBanners_.length()
                && activeBanners_[idx].parentID_ == parentID)
         {
             /* this is a child, so skip it and all of its descendants */
             idx = skipDescendants(idx);
         }
 
         /* return the new index */
         return idx;
     }
 
     /* remove a banner from the active list */
     removeBanner(win)
     {
         local idx;
         local lastIdx;
 
         /* get the index of the item to remove */
         idx = activeBanners_.indexOf(win);
 
         /* if we didn't find it, ignore the request */
         if (idx == nil)
             return;
 
         /* find the index of its last descendant */
         lastIdx = skipDescendants(idx) - 1;
 
         /* 
          *   remove the item and all of its descendants - child items
          *   cannot be displayed once their parents are gone, so we can
          *   remove all of this item's children, all of their children,
          *   and so on, as they are becoming undisplayable 
          */
         activeBanners_.removeRange(idx, lastIdx);
     }
 
     /*
      *   The list of active banners.  This is a list of BannerWindow
      *   objects, stored in banner layout list order. 
      */
     activeBanners_ = static new Vector(32)
 
     /* receive RESTORE/UNDO notification */
     execute()
     {
         /* restore the display state for a non-initial state */
         restoreDisplayState(nil);
     }
 
     /*
      *   Restore the saved banner display state, so that the banner layout
      *   looks the same as it did when we saved the persistent state.  This
      *   should be called after restoring a saved state, undoing to a
      *   savepoint, or initializing (when first starting the game or when
      *   restarting).
      */
     restoreDisplayState(initing)
     {
         local uiVec;
         local uiIdx;
         local origActive;
 
         /* get the list of banners active in the UI */
         uiVec = bannerUITracker.activeBanners_;
 
         /*
          *   First, go through all of the persistent BannerWindow objects.
          *   For each one whose ID shows up in the active UI display list,
          *   tell the BannerWindow object its current UI handle.  
          */
         forEachInstance(BannerWindow, new function(cur)
         {
             local uiCur;
             
             /* find this banner in the active UI list */
             uiCur = uiVec.valWhich({x: x.id_ == cur.getBannerID()});
 
             /* 
              *   if the window exists in the active UI list, note the
              *   current system handle for the window; otherwise, we have
              *   no system window, so set the handle to nil 
              */
             if (uiCur != nil)
             {
                 /* note the current system banner handle */
                 cur.handle_ = uiCur.handle_;
 
                 /* re-establish the banner's active output stream */
                 cur.outputStream_ = uiCur.outputStream_;
 
                 /* tie the transient record to the current 'cur' */
                 uiCur.win_ = cur;
             }
             else
             {
                 /* it's not shown, so it has no system banner handle */
                 cur.handle_ = nil;
 
                 /* it has no output stream */
                 cur.outputStream_ = nil;
             }
         });
 
         /* 
          *   
          *   'initing' indicates whether we're initializing (startup or
          *   RESTART) or doing something else (RESTORE, UNDO).  When
          *   initializing, if there are any banners on-screen, we'll give
          *   their associated BannerWindow objects (if any) a chance to set
          *   up their initial conditions; this allows us to avoid
          *   unnecessary redrawing if we have banners that we'd immediately
          *   set up to the same conditions anyway, since we can just keep
          *   the existing banners rather than removing and re-creating
          *   them.
          *   
          *   So, if we're initializing, tell each banner that it's time to
          *   set up its initial display.  
          */
         if (initing)
             forEachInstance(BannerWindow, {cur: cur.initBannerWindow()});
 
         /* 
          *   scan the active UI list, and close each window that isn't
          *   still open in the saved state 
          */
         foreach (local uiCur in uiVec)
         {
             /* if this window isn't in the active list, close it */
             if (activeBanners_.indexWhich(
                 {x: x.getBannerID() == uiCur.id_}) == nil)
             {
                 /*
                  *   There's no banner in the persistent list with this
                  *   ID, so this window is not part of the state we're
                  *   restoring.  Close the window.  If we have an
                  *   associated BannerWindow object, close through the
                  *   window object; otherwise, close the system handle
                  *   directly.  
                  */
                 if (uiCur.win_ != nil)
                 {
                     /* we have a BannerWindow - close it */
                     uiCur.win_.removeBanner();
                 }
                 else
                 {
                     /* there's no BannerWindow - close the system window */
                     bannerDelete(uiCur.handle_);
 
                     /* remove the UI tracker object */
                     uiVec.removeElement(uiCur);
                 }
             }
         }
 
         /* start at the first banner actually displayed right now */
         uiIdx = 1;
 
         /* 
          *   make a copy of the original active list - we might modify the
          *   actual active list in the course of restoring things, so make
          *   a copy that we can refer to as we reconstruct the original
          *   list 
          */
         origActive = activeBanners_.toList();
 
         /* 
          *   Scan the saved list of banners, and restore each one.  Note
          *   that by restoring windows in the order in which they appear
          *   in the list, we ensure that we always restore a parent before
          *   restoring any of its children, since a child always follows
          *   its parent in the list.  
          */
         for (local curIdx = 1, local aLen = origActive.length() ;
              curIdx <= aLen ; ++curIdx)
         {
             local redisp;
             local cur;
 
             /* get the current item */
             cur = origActive[curIdx];
                 
             /* presume we will have to redisplay this banner */
             redisp = true;
             
             /*
              *   If this banner matches the current banner in the active
              *   UI display list, and the characteristics match, we need
              *   do nothing, as we're already displaying this banner
              *   properly.  If the current active UI banner doesn't match,
              *   then we need to insert this saved banner at the current
              *   active UI position. 
              */
             if (uiVec.length() >= uiIdx)
             {
                 local uiCur;
 
                 /* get this current UI display item (a BannerUIWindow) */
                 uiCur = uiVec[uiIdx];
 
                 /* check for a match to 'cur' */
                 if (uiCur.id_ == cur.getBannerID()
                     && uiCur.parentID_ == cur.parentID_
                     && uiCur.windowType_ == cur.windowType_
                     && uiCur.align_ == cur.align_
                     && uiCur.styleFlags_ == cur.styleFlags_)
                 {
                     /*
                      *   This saved banner ('cur') exactly matches the
                      *   active UI banner ('uiCur') at the same position
                      *   in the layout list.  Therefore, we do not need to
                      *   redisplay 'cur'.
                      */
                     redisp = nil;
                 }
             }
 
             /* if we need to redisplay 'cur', do so */
             if (redisp)
             {
                 local prvIdx;
                 local where;
                 local other;
                 local parent;
 
                 /*   
                  *   If 'cur' is already being displayed, we must remove
                  *   it before showing it anew.  This is the only way to
                  *   ensure that we display it with the proper
                  *   characteristics, since the characteristics of the
                  *   current instance of its window don't match up to what
                  *   we want to restore.  
                  */
                 if (cur.handle_ != nil)
                     cur.removeBanner();
 
                 /*
                  *   Figure out how to specify this window's display list
                  *   position.  A display list position is always
                  *   specified relative to the parent's child list, so
                  *   figure out where we go in our parent's list.  Scan
                  *   backwards in the active list for the nearest previous
                  *   window with the same parent.  If we find one, insert
                  *   the new window after that prior sibling; otherwise,
                  *   insert as the first child of our parent.  Presume
                  *   that we'll fail to find a prior sibling, then search
                  *   for it and search for our parent.  
                  */
                 where = BannerFirst;
                 other = nil;
                 for (prvIdx = curIdx - 1 ; prvIdx > 0 ; --prvIdx)
                 {
                     local prv;
 
                     /* note this item */
                     prv = origActive[prvIdx];
                     
                     /* 
                      *   If this item has our same parent, and we haven't
                      *   already found a prior sibling, this is our most
                      *   recent prior sibling, so note it.  
                      */
                     if (where == BannerFirst
                         && prv.parentID_ == cur.parentID_)
                     {
                         /* insert after this prior sibling */
                         where = BannerAfter;
                         other = prv;
                     }
 
                     /* if this is our parent, note it */
                     if (prv.getBannerID() == cur.parentID_)
                     {
                         /* 
                          *   note the parent BannerWindow object - we'll
                          *   need it to specify our window display
                          *   position 
                          */
                         parent = prv;
 
                         /* 
                          *   Children of a given parent always come after
                          *   the parent in the display list, so there's no
                          *   possibility of finding another sibling.
                          *   There's also obviously no possibility of
                          *   finding another parent.  So, our work here is
                          *   done; we can stop scanning.
                          */
                         break;
                     }
                 }
                     
                 /* show the window */
                 cur.showForRestore(parent, where, other);
             }
 
             /*
              *   'cur' should now be displayed, but we might have failed
              *   to re-create it.  If we did show the window, we can
              *   advance to the next slot in the UI list, since this
              *   window will necessarily be at the current spot in the UI
              *   list.  
              */
             if (cur.handle_ != nil)
             {
                 /* 
                  *   We know that this window is now the entry in the
                  *   active UI list at the current index we're looking at.
                  *   Move on to the next position in the active list for
                  *   the next saved window.  
                  */
                 ++uiIdx;
             }
         }
     }
 ;
 
 /*
  *   Initialization object - this will be called when we start the game the
  *   first time or RESTART within a session.  We'll restore the display
  *   state to the initial conditions. 
  */
 bannerInit: InitObject
     execute()
     {
         /* restore banner displays to their initial conditions */
         bannerTracker.restoreDisplayState(true);
     }
 ;
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11