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: OutputStreamWindow
    /*
     *   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);
    }

    /* 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 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);
    }

    /* create our banner output stream */
    createOutputStreamObj()
    {
        return new transient BannerOutputStream(handle_);
    }

    /*
     *   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

    /* 
     *   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, 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);
            }
            else
            {
                /* 
                 *   the banner is already showing, so we don't need to
                 *   redisplay it; simply notify it that a Restore
                 *   operation has taken place so that it can do any
                 *   necessary updates 
                 */
                cur.updateForRestore();
            }

            /*
             *   '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 5/16/2013 from TADS version 3.1.3