// Copyright (C) 2007-2010 Bristle Software, Inc.
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 1, or (at your option)
// any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.Presentation.js
*******************************************************************************
* Purpose:
*       This file contains JavaScript code to turn a collection of Web pages
*       into a slide show presentation.
* Usage:
*       - The typical scenario for using this file from an HTML file is shown
*         in the file: com.bristle.jslib.Presentation.Example.htm
*       - The easiest way to get started is to go to a presentation at:
*             http://bristle.com/Talks
*         and ask your browser to save the "complete" web page.  It will
*         make local copies of the main index.htm, all the CSS and JS files,
*         including this one, but none of the actual slides -- just the set
*         of files you need to get started.
* Assumptions:
* Effects:
*       - None.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
// Note:  Don't redefine "com" or other objects that may be defined in other 
//        JavaScript files.  Such definitions step on each other and cause 
//        errors like:
//              "com.bristle.jslib is null or not an object."
//        on the first call to any function in the hierarchy.  This can be
//        very difficult to diagnose because the message points you to the 
//        file containing the first executed function call, not necessarily 
//        the file containing the error.  For example, creating the namespace
//        wrong for com.abc can step on the com portion of the namespace for 
//        com.xyz, and cause errors on calls to com.xyz.pdq.mno.function1().
com.bristle.jslib.Presentation = {};

// These are the default settings for the slide show.  They can be 
// overridden in index.htm or someplace for each slide show.
com.bristle.jslib.Presentation.intSlideCount = 0;
com.bristle.jslib.Presentation.noPageOneFooter = true; 
com.bristle.jslib.Presentation.arrstrSlideURLs = new Array();
com.bristle.jslib.Presentation.blnInitialAutoAdvanceMode = false;

// These variables can be set in index.htm or changed on the fly by the user.
com.bristle.jslib.Presentation.blnAdvanceOnMouseClick  = true;
com.bristle.jslib.Presentation.intAutoAdvanceDelaySecs = 10;

// Shortcut keys:  "m"         = Toggle blnAdvanceOnMouseClick
//                 Click       = Next slide (if blnAdvanceOnMouseClick)
//                 "a"         = Toggle blnAutoAdvanceMode
//                 "-"         = Increment  intAutoAdvanceDelaySecs (slower)
//                 "+"         = Decrement intAutoAdvanceDelaySecs (faster) 
//                 "="         = Decrement intAutoAdvanceDelaySecs (faster) 
//                 Enter       = Next slide
//                 Right Arrow = Next slide
//                 Left Arrow  = Previous slide
//                 Home        = First slide
//                 End         = Last slide

// These variables maintain the state of the slideshow and are not typically
// updated outside of this file.
com.bristle.jslib.Presentation.intSlideNumber = 0;
com.bristle.jslib.Presentation.timerAutoAdvance = null;

/******************************************************************************
* Update the footer, based on the slide number.
******************************************************************************/
com.bristle.jslib.Presentation.updateFooter = 
function()
{
    var divFooter = document.getElementById("divFooter");
    if (divFooter)
    {
        var spanSlideNumber = document.getElementById("spanSlideNumber");
        if (spanSlideNumber)
        {
            spanSlideNumber.innerHTML = this.intSlideNumber + 1;
        }
        var spanSlideCount = document.getElementById("spanSlideCount");
        if (spanSlideNumber)
        {
            spanSlideCount.innerHTML = this.intSlideCount;
        }
        if (this.noPageOneFooter)
        {
            var blnVisible = (this.intSlideNumber != 0);
            com.bristle.jslib.Util.setVisible(divFooter, blnVisible);
        }
    }
}

/******************************************************************************
* Load the auto-scaling background image.
******************************************************************************/
var blnUseBackgroundImage = false;
var strImgBackgroundId = "imgBackground";
com.bristle.jslib.Presentation.loadBackgroundImage = 
function()
{
    // If the background image of this slide is a single, non-repeated image 
    // that is supposed to remain fixed when the rest of the content scrolls,
    // replace it with an image scaled to the exact size of the window.
    // This is necessary because CSS offers no option to scale a 
    // background image to fit the window, only options to repeat it
    // to fill the window with copies of it.
    blnUseBackgroundImage = false;
    var imgBackground = document.getElementById(strImgBackgroundId);
    var objStyle = com.bristle.jslib.Util.getStyle(document.body);
    var strBackgroundRepeat     = objStyle.backgroundRepeat;
    var strBackgroundAttachment = objStyle.backgroundAttachment;
    if (   strBackgroundRepeat == "no-repeat" 
        && strBackgroundAttachment == "fixed") 
    {
        var strURL_PREFIX = "url(";
        var strURL_SUFFIX = ")";
        var strBackgroundImageURL 
            = com.bristle.jslib.Util.trim(objStyle.backgroundImage);
        if (  !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                       (strBackgroundImageURL)
            && com.bristle.jslib.Util.startsWith
                                       (strBackgroundImageURL, strURL_PREFIX)
            && com.bristle.jslib.Util.endsWith
                                       (strBackgroundImageURL, strURL_SUFFIX)
           )
        {
            strBackgroundImageURL 
                 = com.bristle.jslib.Util.trim
                        (strBackgroundImageURL.slice
                              (strURL_PREFIX.length    // Start after leading "url("
                              ,- strURL_SUFFIX.length  // End before trailing ")"
                              )
                        );
            // Some browsers (IE6, IE8, and perhaps others) return the 
            // background-image URL as:
            //    url("http://...")
            // not just:
            //    url(http://...)
            // Strip off the extra quotes (both styles for good measure).
            if ((   com.bristle.jslib.Util.startsWith
                                       (strBackgroundImageURL, '"')
                 && com.bristle.jslib.Util.endsWith
                                       (strBackgroundImageURL, '"')
                )
                ||
                (   com.bristle.jslib.Util.startsWith
                                       (strBackgroundImageURL, "'")
                 && com.bristle.jslib.Util.endsWith
                                       (strBackgroundImageURL, "'")
                )
               )
            {
                strBackgroundImageURL 
                 = com.bristle.jslib.Util.trim
                        (strBackgroundImageURL.slice(1, -1));
            }
            if (strBackgroundImageURL != "")
            {
                if (!imgBackground)
                {
                    imgBackground = document.createElement("img");
                    imgBackground.id = strImgBackgroundId;
                    com.bristle.jslib.Layout.simulateCSSPositionFixed
                            (imgBackground, 0, 0);
                    imgBackground.style.zIndex = -1;
                    // Note: Use insertBefore(), not appendChild(), to make
                    //       it the fitst child, not the last.  Otherwise, 
                    //       some browsers (IE6, IE8 and probably others)
                    //       don't put it at the top left of the window.
                    document.body.insertBefore
                                   (imgBackground, document.body.firstChild);
                }
                imgBackground.src = strBackgroundImageURL;
                blnUseBackgroundImage = true;
            }
        }
    }
}

/******************************************************************************
* Get the URL of the next slide, or the specified slide.
*
*@param intOffset   Numeric offset from the current slide to the next slide.
*                   Default: 1 (the next sequential slide)
*                   Other typical values:
*                     0  = Special flag meaning the first slide
*                     -1 = Previous seqential slide
*                   Large values wrap around, so specifying 1 multiple times
*                   cycles through all slides, eventually looping back to
*                   the first slide, and specifying a negative number 
*                   eventually wraps around past the first slide to the last. 
******************************************************************************/
com.bristle.jslib.Presentation.getNextSlideURL = 
function(intOffset)
{
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(intOffset))
    {
        intOffset = 1;
    }
    if (intOffset == 0)
    {
        this.intSlideNumber = intOffset;
    }
    else
    {
        // Note: Do not assign any temporary values to this.intSlideNumber.
        //       Compute the entire value before assigning it.  Until the mod 
        //       function is applied, the value may be out of range of the
        //       array this.arrstrSlideURLs[] which can cause this function 
        //       to return an undefined value, so an attempt is made by the
        //       caller to load a URL ending in the string "undefined".
        //       This can happen because this code can run concurrently from 
        //       multiple threads.
        var intSlideNumber = (this.intSlideNumber + intOffset) 
                            % this.intSlideCount;
        if (intSlideNumber < 0)
        {
            intSlideNumber += this.intSlideCount;
        }
        this.intSlideNumber = intSlideNumber;
    }
    return this.arrstrSlideURLs[this.intSlideNumber];
}

/******************************************************************************
* Load the next slide, or the specified slide.
*
*@param intOffset     Numeric offset from the current slide to the next slide.
*                     Default: 1 (the next sequential slide)
*                     Other typical values:
*                       0  = Special flag meaning the first slide
*                       -1 = Previous sequential slide
*                     Large values wrap around, so specifying 1 multiple times
*                     cycles through all slides, eventually looping back to
*                     the first slide.
*@param intRetryCount Number of times already tried.  Default: 0
*@return              True if slide successfully loaded, false if an error 
*                     occurred or still trying.
******************************************************************************/
com.bristle.jslib.Presentation.loadNextSlide = 
function(intOffset, intRetryCount)
{
    // Prevent infinite retries.
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(intRetryCount))
    {
        intRetryCount = 0;
    }
    var intRETRY_DELAY_MILLISECONDS     = 10;
    var intONE_SECOND                   = 1000;
    var intMAX_TOTAL_DELAY_MILLISECONDS = 5 * intONE_SECOND;
    var intMAX_RETRIES                  = intMAX_TOTAL_DELAY_MILLISECONDS 
                                        / intRETRY_DELAY_MILLISECONDS;
    if (intRetryCount >= intMAX_RETRIES)
    {
        window.status = "Unable to load frame body after "
                      + intRetryCount 
                      + " attempts in "
                      + intMAX_TOTAL_DELAY_MILLISECONDS / intONE_SECOND
                      + " seconds."
                      ;
        return false;
    }

    // Only set src once.  That triggers the load from the URL.  After that,
    // we're just waiting for that load to complete.  Don't set src again
    // because that might restart the load.
    var frameHidden = document.getElementById("frameHidden");
    if (intRetryCount == 0)
    {
        // Clear the old slide so we can tell when the new slide arrives.
        if (   frameHidden.contentWindow 
            && frameHidden.contentWindow.document
            && frameHidden.contentWindow.document.body
           )
        {
            frameHidden.contentWindow.document.body.innerHTML = "";
        }

        var strSrc = this.getNextSlideURL(intOffset);
        frameHidden.onload = function()
                {
                    com.bristle.jslib.Presentation.loadNextSlide
                      (intOffset, ++intRetryCount);
                }
        frameHidden.src = strSrc;
    }

//?? Delete this.  Do it only on onload of iframe?
    // Wait for the frame to load its new body
    if (   !frameHidden.contentWindow
        || !frameHidden.contentWindow.document
        || !frameHidden.contentWindow.document.body
        || (frameHidden.contentWindow.document.body.innerHTML == ""))
    {
        window.status = "Loading" 
                      + com.bristle.jslib.Util.makeStringOfChars
                      	(".", intRetryCount);
        window.setTimeout
                    ("com.bristle.jslib.Presentation.loadNextSlide("
                     + intOffset + "," + (++intRetryCount) + ")"
                    , intRETRY_DELAY_MILLISECONDS
                    );
        return false;
    }

    // The slide has now loaded from the URL into the FRAME.
    // Copy the body of the slide from the hidden FRAME to the visible  DIV.
    window.status = ""; 
    var divSlideContent = document.getElementById("divSlideContent");
//??debugPrint("4: divSlideContent.offsetHeight = " + divSlideContent.offsetHeight);
    divSlideContent.innerHTML = frameHidden.contentWindow.document.body.innerHTML;
//?? Why is the offsetHeight still zero in Safari, Chrome, and Netscape?  
//?? Setting innerHTML should have given it some height.
//?? See comments in com.bristle.jslib.Layout.getOuterHeight.
//??debugPrint("5: divSlideContent.offsetHeight = " + divSlideContent.offsetHeight);
    document.body.className = frameHidden.contentWindow.document.body.className;

    window.scrollTo(0,0);

    this.loadBackgroundImage();

    // Simulate a resize event to update the footer, including resizing 
    // and repositioning it, in case the font has changed since the 
    // previous slide.
    this.objBody_onResize();

    return true;
}


/******************************************************************************
* Dynamically build the slide.
* Usage:  Typically called as the onload event of the HTML BODY tag, as:
*         <body onLoad='com.bristle.jslib.Presentation.objBody_onLoad()'>
*
*@param intRetryCount Number of times already tried.  Default: 0
*@return              True if slide successfully loaded, false if an error 
*                     occurred or still trying.
******************************************************************************/
com.bristle.jslib.Presentation.objBody_onLoad = 
function(intRetryCount)
{
    // Prevent infinite retries.
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(intRetryCount))
    {
        intRetryCount = 0;
    }
    var intRETRY_DELAY_MILLISECONDS     = 10;
    var intONE_SECOND                   = 1000;
    var intMAX_TOTAL_DELAY_MILLISECONDS = 5 * intONE_SECOND;
    var intMAX_RETRIES                  = intMAX_TOTAL_DELAY_MILLISECONDS 
                                        / intRETRY_DELAY_MILLISECONDS;
    if (intRetryCount >= intMAX_RETRIES)
    {
        window.status = "Unable to load default frame body after "
                      + intRetryCount 
                      + " attempts in "
                      + intMAX_TOTAL_DELAY_MILLISECONDS / intONE_SECOND
                      + " seconds."
                      ;
        return false;
    }

    // By default, the slideshow runs through all specified slides, but
    // that can be overridden in index.htm or someplace, by setting 
    // this.intSlideCount
    if (this.intSlideCount == 0)
    {
        this.intSlideCount = this.arrstrSlideURLs.length;
    }

    // Create a visible DIV, if it doesn't already exist, to use in displaying 
    // the slide contents.
    var strDivSlideContentId = "divSlideContent";
    var divSlideContent = document.getElementById(strDivSlideContentId);
    if (!divSlideContent)
    {
        divSlideContent = document.createElement("div");
        divSlideContent.id = strDivSlideContentId;
        document.body.appendChild(divSlideContent);
    }

    // Set the DIV properties, whether static or dynamic
    divSlideContent.className 
                    = "com_bristle_jslib_css_Presentation_slideContentDIV";
    com.bristle.jslib.Util.setVisible(divSlideContent, true);

    // Create a hidden IFRAME, if it doesn't already exist, to use in loading 
    // the slide contents.
    // Note: It is more reliable to have it already exist in the static HTML:
    //           <iframe id='frameHidden'></iframe>
    //       Otherwise, IE 6 and 8 (and perhaps others, though Firefox seems 
    //       OK) sometimes reports errors that the document of the frame 
    //       exists, but has no body.  That is, the following is null:
    //           frameHidden.contentWindow.document.body
    //       This is because this method is typically called from the onload 
    //       event of the HTML BODY tag, which means that the body has 
    //       finished loading its contents from the Web server, but the nested 
    //       frames may not yet have finished loading their contents.
    var strFrameHiddenId = "frameHidden";
    var frameHidden = document.getElementById(strFrameHiddenId);
    if (!frameHidden)
    {
        // Note: Must add to document body.  Otherwise, the object 
        //       frameHidden.contentWindow is undefined later.
        frameHidden = document.createElement("iframe");
        frameHidden.id = strFrameHiddenId;
        // Note: Hide it before adding to the document body.  Otherwise
        //       it is briefly visible.
        com.bristle.jslib.Util.setVisible(frameHidden, false);
        document.body.appendChild(frameHidden);
    }

    // Hide the frame, whether static or dynamic
    com.bristle.jslib.Util.setVisible(frameHidden, false);

    // Wait for the frame to load its initial default body
    if ((frameHidden.src == "")
        &&
        (   !frameHidden.contentWindow 
         || !frameHidden.contentWindow.document
         || !frameHidden.contentWindow.document.body
        )
       )
    {
        window.status = "Loading" 
                      + com.bristle.jslib.Util.makeStringOfChars
                      	(".", intRetryCount);
        window.setTimeout
                    ("com.bristle.jslib.Presentation.objBody_onLoad("
                     + (++intRetryCount) + ")"
                    , intRETRY_DELAY_MILLISECONDS
                    );
        return false;
    }

    // Simulate an initial resize event to do the parts of initialization 
    // that are repeated at each resize.
    this.objBody_onResize();

    // Load the hidden FRAME from the URL of the next slide, if not already 
    // done.
    // Note: This happens asynchronously, so don't count on it finishing 
    //       immediately.
    if (frameHidden.src == "")
    {
        this.loadNextSlide(0);
    }

    // Periodically simulate a resize event to update the footer, including 
    // resizing and repositioning it, in case the user has changed the font
    // size via Ctrl-Plus/Minus, which triggers no event.
    window.setInterval("com.bristle.jslib.Presentation.objBody_onResize(true)"
                      ,intONE_SECOND
                      );

    // Optionally, start in auto advance mode.
    if (this.blnInitialAutoAdvanceMode)
    {
        this.resetAutoAdvanceTimer(this.blnSTART_NEW_TIMER);
    }
    return true;
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.intFooterLeft     = null; 
com.bristle.jslib.Presentation.intFooterTop      = null; 
com.bristle.jslib.Presentation.intWindowWidth    = null; 
com.bristle.jslib.Presentation.intWindowHeight   = null; 
com.bristle.jslib.Presentation.intDocumentWidth  = null; 
com.bristle.jslib.Presentation.intDocumentHeight = null; 
com.bristle.jslib.Presentation.objBody_onResize = 
function(blnTimer)
{
    // Nothing to do if the window size and document size haven't changed.
    // Note: This is important too, because this routine is called from a 
    //       periodic timer.  Redoing things unnecessary causes the footer 
    //       to come and go, which causes scrolling.  Looks bad.
    // Note: Act when the document size changes also, not just the window
    //       size.  That catches font size changes, images that load 
    //       slowly, etc.
    // Note: Act whenever it gets called for real, not from the timer,
    //       regardless of size changes.
    if (blnTimer
        &&
        this.intWindowWidth    == com.bristle.jslib.Layout.getInnerWindowWidth()
        &&
        this.intWindowHeight   == com.bristle.jslib.Layout.getInnerWindowHeight()
        &&
        this.intDocumentWidth  == com.bristle.jslib.Layout.getDocumentWidth()
        &&
        this.intDocumentHeight == com.bristle.jslib.Layout.getDocumentHeight()
       )
    {
        return true;
    }
    this.intWindowWidth    = com.bristle.jslib.Layout.getInnerWindowWidth();
    this.intWindowHeight   = com.bristle.jslib.Layout.getInnerWindowHeight();
    this.intDocumentWidth  = com.bristle.jslib.Layout.getDocumentWidth();
    this.intDocumentHeight = com.bristle.jslib.Layout.getDocumentHeight();

    // Size and position the footer, if any, to stay at the bottom of the page.
    var divFooter = document.getElementById("divFooter");
    if (divFooter)
    {
        // Use CSS file settings for ease of change, then override the ones 
        // that matter, and the ones that can't be specified statically in the 
        // CSS file because they must be computed dynamically.
        divFooter.className = "com_bristle_jslib_css_Presentation_footerDIV";
        divFooter.style.position = "fixed";

        // Record the original left position of the footer for use in calling
        // com.bristle.jslib.Layout.simulateCSSPositionFixed()
        if (this.intFooterLeft == null)
        {
            this.intFooterLeft = divFooter.offsetLeft;
        }

        // Note: Call updateFooter() to update the visibility and contents 
        //       of the footer, before sizing and positioning it.
        //       getAbsoluteLeft() doesn't work for invisible elements.
        this.updateFooter();

        var divSlideContent = document.getElementById("divSlideContent");
        var intFooterHeight 
                = com.bristle.jslib.Layout.getOuterHeight(divFooter);
        var intContentHeight 
                = com.bristle.jslib.Layout.getOuterHeight(divSlideContent);
        var intInnerWindowHeight 
                = com.bristle.jslib.Layout.getInnerWindowHeight()
        this.intFooterTop = intInnerWindowHeight - intFooterHeight;
        var intContentTop 
                = com.bristle.jslib.Layout.getAbsoluteTop(divSlideContent);
        var intContentBottom = intContentTop + intContentHeight;

        //?? In some browsers, com.bristle.jslib.Layout.getOuterHeight returns
        //?? zero here.  See it's comments for details.  If so, assume the 
        //?? content fills the window.  Otherwise, the logic later in this 
        //?? routine fails to prevent the footer from overlapping the content.
        if (intContentHeight == 0)
        {
            intContentBottom = intInnerWindowHeight;
        }

        if (this.intFooterTop > intContentBottom)
        {
            // The slide content fits without scrolling.  Put the footer at 
            // the bottom of the slide in a fixed position.
            com.bristle.jslib.Layout.simulateCSSPositionFixed
                            (divFooter
                            ,this.intFooterLeft
                            ,this.intFooterTop
                            );
        }
        else
        {
            // The slide is too long and will scroll.  Put the footer after it.
            divFooter.style.position = "static";
            divFooter.style.left = this.intFooterLeft;
            divFooter.style.top = 0;
        }
        divFooter.style.width
                    = com.bristle.jslib.Layout.getInnerWindowWidth()
                    - com.bristle.jslib.Layout.getAbsoluteLeft(divFooter)
                    ;
    }
    
    // Resize the background image to fill the entire window. 
    var imgBackground = document.getElementById(strImgBackgroundId);
    if (blnUseBackgroundImage)
    {
        imgBackground.width = com.bristle.jslib.Layout.getInnerWindowWidth();
        imgBackground.height = com.bristle.jslib.Layout.getInnerWindowHeight();
    }
    
    // Hide/show the resized background image. 
    if (imgBackground)
    {
        com.bristle.jslib.Util.setVisible(imgBackground, blnUseBackgroundImage);
    }

    return true;
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.objBody_onScroll = 
function(evt, obj)
{
    var imgBackground = document.getElementById(strImgBackgroundId);
    if (blnUseBackgroundImage)
    {
        com.bristle.jslib.Layout.simulateCSSPositionFixed(imgBackground, 0, 0);
    }

    // Reposition the footer at each scroll operation.
    var divFooter = document.getElementById("divFooter");
    if (com.bristle.jslib.Util.getStyle(divFooter).position == "absolute")
    {
        com.bristle.jslib.Layout.simulateCSSPositionFixed
                            (divFooter
                            ,this.intFooterLeft
                            ,this.intFooterTop
                            );
    }
    return true;
}

/******************************************************************************
* Clear and optionally re-start the auto advance timer.
*
*@param blnRestart   Re-start the timer if true
******************************************************************************/
com.bristle.jslib.Presentation.blnSTART_NEW_TIMER = true; 
com.bristle.jslib.Presentation.resetAutoAdvanceTimer = 
function(blnRestart)
{
    if (this.timerAutoAdvance != null)
    {
        window.clearInterval(this.timerAutoAdvance);
        this.timerAutoAdvance = null;
    }

    if (blnRestart)
    {
        this.timerAutoAdvance = window.setInterval
                ("com.bristle.jslib.Presentation.loadNextSlide(+1)"
                ,this.intAutoAdvanceDelaySecs * 1000
                );
    }
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.blnALLOW_DEFAULT_ACTION    = true;
com.bristle.jslib.Presentation.blnSUPPRESS_DEFAULT_ACTION = false;
com.bristle.jslib.Presentation.blnKeyPressExpected = false;
com.bristle.jslib.Presentation.objBody_onKeyPress = 
function(evt, obj)
{
    this.blnKeyPressExpected = false;

    // Note: Use onKeyPress, not onKeyUp, or onKeyDown, because it is the 
    //       only one that autorepeats for non-IE browsers.

    var intKeyCode = com.bristle.jslib.Event.getEventKeyCode(evt);
    if (false);
    else if (evt.ctrlKey || evt.altKey || evt.metaKey)
        // Note:  Do NOT add evt.shiftKey here.
    {
        // Don't intercept Ctrl, Alt, or Meta versions of the keys.
        // Only regular keys, and Shifted keys like (like "+").
        // Modified keys like Ctrl-Plus and Ctrl-Minus, still do their 
        // regular browser functions of changing font size.  Ctrl-Home
        // and Ctrl-End still do their regular functions of going to the 
        // beginning and end of the page, etc.
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (intKeyCode == "m".charCodeAt(0))
    {
        // Lowercase "m" toggles use of mouse clicks to advance pages.
        this.blnAdvanceOnMouseClick = !this.blnAdvanceOnMouseClick;
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (intKeyCode == "a".charCodeAt(0))
    {
        // Lowercase "a" toggles automatic advance mode.
        if (this.timerAutoAdvance == null)
        {
            this.resetAutoAdvanceTimer(this.blnSTART_NEW_TIMER);
        }
        else
        {
            this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
        }
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (   intKeyCode == "+".charCodeAt(0)
             || intKeyCode == "=".charCodeAt(0)
            )
    {
        // "+" or "=" (unshifted "+") speeds up the auto advance.
        var intTHREE_SECONDS = 3;
        this.intAutoAdvanceDelaySecs 
                   = Math.max(intTHREE_SECONDS, this.intAutoAdvanceDelaySecs-1);
        this.resetAutoAdvanceTimer(this.blnSTART_NEW_TIMER);
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (intKeyCode == "-".charCodeAt(0))
    {
        // "-" slows down the auto advance.
        var intONE_HOUR = 60*60;
        this.intAutoAdvanceDelaySecs 
                   = Math.min(intONE_HOUR, this.intAutoAdvanceDelaySecs+1);
        this.resetAutoAdvanceTimer(this.blnSTART_NEW_TIMER);
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (evt.shiftKey)
    {
        // Don't intercept Shifted versions of the remaining keys.
        // Only regular keys.  Shifted versions of Left and Right arrows
        // still do their regular browser functions of selecting text, etc.
        return this.blnALLOW_DEFAULT_ACTION;
    }
    else if (intKeyCode == com.bristle.jslib.Event.intKEYCODE_LEFT_ARROW)
    {
        // Left arrow key moves to previous slide and cancels auto advance.
        this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
        this.loadNextSlide(-1);
        return this.blnSUPPRESS_DEFAULT_ACTION;
    }
    else if (   intKeyCode == com.bristle.jslib.Event.intKEYCODE_RIGHT_ARROW
             || intKeyCode == com.bristle.jslib.Event.intKEYCODE_ENTER
            )
    {
        // Right array key and Enter key move to next slide and cancel 
        // auto advance.
        this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
        this.loadNextSlide(+1);
        return this.blnSUPPRESS_DEFAULT_ACTION;
    }
    else if (intKeyCode == com.bristle.jslib.Event.intKEYCODE_HOME)
    {
        // Home key moves to first slide and cancels auto advance.
        this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
        this.loadNextSlide(0);
        return this.blnSUPPRESS_DEFAULT_ACTION;
    }
    else if (intKeyCode == com.bristle.jslib.Event.intKEYCODE_END)
    {
        // End key moves to last slide and cancels auto advance.
        this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
        var intOffset = this.intSlideCount - this.intSlideNumber - 1;
        if (intOffset == 0)
        {
            // Nothing to do.  We're already at the last slide.
        }
        else
        {
            this.loadNextSlide(intOffset);
        }
        return this.blnSUPPRESS_DEFAULT_ACTION;
    }
    else if (intKeyCode == com.bristle.jslib.Event.intKEYCODE_UP_ARROW)
    {
        // Up arrow key is left alone.  It already scrolls the text.
    }
    else if (intKeyCode == com.bristle.jslib.Event.intKEYCODE_DOWN_ARROW)
    {
        // Down arrow key is left alone.  It already scrolls the text.
    }
    return this.blnALLOW_DEFAULT_ACTION;
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.objBody_onKeyDown =
function(evt, obj)
{
    this.blnKeyPressExpected = true;
    
    // Don't intercept modified keys.  Only regular keys.  Modified keys 
    // like Ctrl-Home, Ctrl-End, etc., still do their regular functions of
    // going to the beginning and end of the page, etc.
    if (evt.ctrlKey || evt.shiftKey || evt.altKey || evt.metaKey)
    {
        return this.blnALLOW_DEFAULT_ACTION;
    }

    var intKeyCode = com.bristle.jslib.Event.getEventKeyCode(evt);
    if (   intKeyCode == com.bristle.jslib.Event.intKEYCODE_LEFT_ARROW
        || intKeyCode == com.bristle.jslib.Event.intKEYCODE_RIGHT_ARROW
        || intKeyCode == com.bristle.jslib.Event.intKEYCODE_HOME
        || intKeyCode == com.bristle.jslib.Event.intKEYCODE_END)
    {
        // Call onKeyPress to handle arrow keys for IE because IE doesn't
        // call it for those keys.  However, don't do that for other browsers
        // because it causes a double event.
        // Without this, the arrow keys would not work in IE.
        // Note: No particular reason to use evt.stopPropagation as the 
        //       technique for detecting whether it is IE or not, except that
        //       it is a standard event-related method that IE doesn't define.
        //       If IE is ever revamped to be standards-compliant, it will
        //       start generating onKeyPress events for arrow keys, and may
        //       at the same time define evt.stopPropagation, so this will
        //       still work.
        if (typeof(evt.stopPropagation) == "undefined")
        {
            var blnAllowDefaultKeyAction = this.objBody_onKeyPress(evt, obj);
            this.blnKeyPressExpected = false;
            return blnAllowDefaultKeyAction;
        }
        return this.blnSUPPRESS_DEFAULT_ACTION; 
    }
    return this.blnALLOW_DEFAULT_ACTION;
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.objBody_onKeyUp =
function(evt, obj)
{
    // Fake onKeyPress event for situations and browsers where onKeyDown and
    // onKeyUp occur, but onKeyPress does not occur between them.
    if (this.blnKeyPressExpected)
    {
        var blnAllowDefaultKeyAction = this.objBody_onKeyPress(evt, obj);
        this.blnKeyPressExpected = false;
        return blnAllowDefaultKeyAction;
    }
    return this.blnALLOW_DEFAULT_ACTION;
}

/******************************************************************************
* Event procedure
******************************************************************************/
com.bristle.jslib.Presentation.divSlideContent_onMouseUp =
function(evt, obj)
{
    // Do only the default action when a link is clicked
    //?? Should really test all elements enclosing the target as well, to 
    //?? see if any of them is a link.
    //?? Should really test that the "A" element has an href property.  If 
    //?? it's just a named anchor, not a link, it has no default action,
    //?? so we should proceed.
    var objTarget = com.bristle.jslib.Event.target(evt);
    if (com.bristle.jslib.Util.isOrIsInsideHTMLElement(objTarget, "A"))
    {
        return this.blnALLOW_DEFAULT_ACTION;
    }

    if (!this.blnAdvanceOnMouseClick)
    {
        return this.blnALLOW_DEFAULT_ACTION;
    }

    // Don't respond to MouseUp that ends a drag to select.
    try
    {
        if (com.bristle.jslib.Util.aRangeIsSelected())
        {
            return this.blnALLOW_DEFAULT_ACTION;
        }
    }
    catch(exception)
    {
        // Ignore the exception caused by an unsupported browser.
        // Assume text is not selected.
    }

    // Load the next slide and stop auto advance mode.
    this.loadNextSlide(+1);
    this.resetAutoAdvanceTimer(!this.blnSTART_NEW_TIMER);
    return this.blnSUPPRESS_DEFAULT_ACTION;
    
}

/******************************************************************************
* Display a debug message.
******************************************************************************/
function debugPrint(strMessage)
{
    com.bristle.jslib.MsgBox.debugPrint(strMessage);
}
