browser.t

documentation
#charset "us-ascii"

/* 
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
 *   
 *   TADS 3 Library - browser (Web UI) input/output manager
 *   
 *   This module defines the low-level functions for handling input and
 *   output via the Web UI.
 *   
 *   The functions in this module are designed primarily for internal use
 *   within the library itself.  Games should use the higher level objects
 *   and functions defined in input.t and output.t instead of directly
 *   calling the functions defined here.  The reason for separating these
 *   functions is to make the UI selection pluggable, so that the same game
 *   can be compiled for either the traditional UI or the Web UI simply by
 *   plugging in the correct i/o module.  
 */

/* include the library header */
#include "advlite.h"


/* ------------------------------------------------------------------------ */
/*
 *   Browser globals 
 */
transient browserGlobals: object
    /* the HTTPServer object for the browser UI session */
    httpServer = nil

    /* 
     *   Log file handle.  For a LogTypeTranscript file, this is a
     *   LogConsole object; for other types, it's a regular file handle.  
     */
    logFile = nil
    
    /* logging type (LogTypeXxx from tadsio.h, or nil if not logging) */
    logFileType = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Initialize the user interface.  The library calls this once at the
 *   start of the interpreter session to set up the UI.  For the Web UI, we
 *   create the HTTP server and send connection instructions to the client.
 */
initUI()
{
    /* 
     *   Set up the HTTP server.  Listen on the launch address, which is
     *   the address that the client used to reach the external Web server
     *   that launched the interpreter.  For local stand-alone launches,
     *   the launch address is nil, so the HTTP server will listen on
     *   localhost, which is just what we need in order to connect to the
     *   local UI.  
     */
    local srv = browserGlobals.httpServer = new HTTPServer(
        getLaunchHostAddr(), nil, 1024*1024);
    
    /* send connection instructions to the client */
    webSession.connectUI(srv);
}

/*
 *   Initialize the display.  We call this when we first enter the
 *   interpreter, and again at each RESTART, to set up the main game
 *   window's initial layout.  We set up the conventional IF screen layout,
 *   with the status line across the top and the transcript/command window
 *   filling the rest of the display.  
 */
initDisplay()
{
    /* set up the command window and status line */
    webMainWin.createFrame(commandWin, 'command',
                           '0, statusline.bottom, 100%, 100%');
    webMainWin.createFrame(statuslineBanner, 'statusline',
                           '0, 0, 100%, content.height');

    /* capture the title string */
    local title = mainOutputStream.captureOutput(
        {: gameMain.setGameTitle() });

    /* parse out the contents of the <title> tag */
    if (rexSearch('<nocase>[<]title[>](.*)[<]/title[>]', title))
        title = rexGroup(1)[3];

    /* initialize the statusline window object */
    statuslineBanner.init();
    statusLine.statusDispMode = StatusModeBrowser;

    /* set the title */
    webMainWin.setTitle(title);

    /* get the session parameters from the arguments */
    local arg = libGlobal.getCommandSwitch('-gameid=');
    if (arg != nil && arg != '')
        webSession.launcherGameID = arg;

    arg = libGlobal.getCommandSwitch('-storagesid=');
    if (arg != nil && arg != '')
        webSession.storageSID = arg;

    arg = libGlobal.getCommandSwitch('-username=');
    if (arg != nil && arg != '')
        webSession.launcherUsername = arg;
}

/*
 *   Shut down the user interface.  The library calls this when the game is
 *   about to terminate. 
 */
terminateUI()
{
    /* if we have an HTTP server, shut it down */
    if (browserGlobals.httpServer != nil)
    {
        /* flush our windows */
        webMainWin.flushWin();
        commandWin.sendWinEvent('<scrollToBottom/>');

        /* end any scripting */
        aioSetLogFile(nil, LogTypeTranscript);
        aioSetLogFile(nil, LogTypeCommand);

        /* 
         *   keep running for a few more minutes, to give clients a chance
         *   to perform final tasks like downloading log files 
         */
        ClientSession.shutdownWait(5*60*1000);

        /* send the shutdown message */
        eventPage.sendEvent('<shutdown/>');

        /* wait a short time for clients to process the shutdown event */
        ClientSession.shutdownWait(5*1000);

        /* shut down the http server */
        browserGlobals.httpServer.shutdown(true);
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Check to see if we're in HTML mode 
 */
checkHtmlMode()
{
    /* 
     *   The web UI is always in HTML mode.  This is regardless of the
     *   interpreter class, because that only tells us about the
     *   interpreter's own native UI.  The actual user interface in Web UI
     *   mode runs in a separate Web browser app, which is inherently HTML
     *   capable. 
     */
    return true;
}

/* ------------------------------------------------------------------------ */
/*
 *   Write text to the main game window
 */
aioSay(txt)
{
    /* write the text to the main command window */
    commandWin.write(txt);

    /* if we're logging a full transcript, write the text */
    if (browserGlobals.logFileType == LogTypeTranscript)
        browserGlobals.logFile.writeToStream(txt);
}

/* ------------------------------------------------------------------------ */
/*
 *   Is a script file active? 
 */
readingScript()
{
    return setScriptFile(ScriptReqGetStatus) != nil;
}

/*
 *   Is an event script active? 
 */
readingEventScript()
{
    local s = setScriptFile(ScriptReqGetStatus);
    return (s != nil && (s & ScriptFileEvent) != 0);
}

/* ------------------------------------------------------------------------ */
/*
 *   Get a line of input from the keyboard, with timeout 
 */
aioInputLineTimeout(timeout)
{
    /* check for script input */
    local scriptMode = setScriptFile(ScriptReqGetStatus);
    if (scriptMode != nil)
    {
        /* we're in a script, so use the regular input line reader */
        local e = inputLineTimeout(timeout);

        /* 
         *   If it's not an end-of-file indication, return the event.  An
         *   EOF means that there are no more events in the script, so
         *   return to reading from the live client UI.  
         */
        if (e[1] != InEvtEof)
        {
            /* echo the input if we're not in quiet mode */
            if (e[1] == InEvtLine && !(scriptMode & ScriptFileQuiet))
                aioSay(e[2].htmlify() + '\n');

            /* log and return the event */
            return aioLogInputEvent(e);
        }
    }

    /* 
     *   read an input line event from the main command window, log it, and
     *   return it 
     */
    return aioLogInputEvent(commandWin.getInputLine(timeout));
}

/*
 *   Cancel a suspended input line 
 */
aioInputLineCancel(reset)
{
    /* cancel the input line in the command window */
    commandWin.cancelInputLine(reset);
}


/* ------------------------------------------------------------------------ */
/*
 *   Read an input event 
 */
aioInputEvent(timeout)
{
    /* check for script input */
    if (readingEventScript())
    {
        /* we're in a script, so use the regular input line reader */
        local e = inputEvent(timeout);

        /* 
         *   If it's not an end-of-file indication, return the event.  An
         *   EOF means that there are no more events in the script, so
         *   return to reading from the live client UI.  
         */
        if (e[1] != InEvtEof)
            return aioLogInputEvent(e);
    }

    /* read an event from the main command window, log it, and return it */
    return aioLogInputEvent(webMainWin.getInputEvent(timeout));
}


/* ------------------------------------------------------------------------ */
/*
 *   Show a "More" prompt 
 */
aioMorePrompt()
{
    /* show a More prompt in the main command window */
    commandWin.showMorePrompt();
}

/* ------------------------------------------------------------------------ */
/*
 *   Clear the screen 
 */
aioClearScreen()
{
    /* clear the main transcript window */
    commandWin.clearWindow();
}


/* ------------------------------------------------------------------------ */
/*
 *   Show a file selector dialog 
 */
aioInputFile(prompt, dialogType, fileType, flags)
{
    /* 
     *   First, try reading from the local console.  Even though we're
     *   using the Web UI, there are two special cases where the input will
     *   come from the local (server-side) console instead of from the
     *   browser UI:
     *   
     *   1. We're reading from an event script.  In this case, regardless
     *   of the UI mode, the interpreter reads from a server-side file and
     *   parses the results into an inputFile() result, bypassing any UI
     *   interaction.
     *   
     *   2. We're running in the Web UI's local stand-alone configuration,
     *   where the browser is actually an integrated window within the
     *   interpreter.  This configuration simulates the traditional UI by
     *   running everything locally - the client and server are running on
     *   the same machine, so there's really no distinction between
     *   client-side and server-side.  Because everything's local, files
     *   are local, so we want to display traditional local file selector
     *   dialogs.  The stand-alone interpreter does this for us via the
     *   standard inputFile() function when it detects this configuration.
     *   
     *   If neither of these special cases apply, inputFile() will return
     *   an error to let us know that it can't show a file dialog in the
     *   current configuration, so we'll continue on to showing the dialog
     *   on the client side via the Web UI.  
     */
    local f = inputFile(prompt, dialogType, fileType, flags);

    /* if that failed, forget the result */
    if (f[1] == InFileFailure)
        f = nil;

    /* if we got a file, check for warnings */
    if (f != nil && f.length() >= 4 && f[4] != nil)
    {
        /* keep going until we get a definitive answer */
        for (local done = nil ; !done ; )
        {
            /* show the warning dialog */
            local d = webMainWin.getInputDialog(
                InDlgIconWarning,
                libMessages.inputFileScriptWarning(f[4], f[2]),
                libMessages.inputFileScriptWarningButtons, 1, 3);
            
            /* check the result */
            switch (d)
            {
            case 0:
            case 3:
                /* dialog error or Cancel Script - stop the script */
                setScriptFile(nil);
                
                /* return a Cancel result */
                return [InFileCancel];
                
            case 1:
                /* "Yes" - proceed */
                done = true;
                break;
                
            case 2:
                /* Choose New File button - show a file dialog */
                local fNew = webMainWin.getInputFile(
                    prompt, dialogType, fileType, flags);
                
                switch (fNew[1])
                {
                case InFileSuccess:
                    /* success - use the new file, and we're done */
                    f = fNew;
                    done = true;
                    break;
                    
                case InFileCancel:
                    /* cancel - repeat the prompt */
                    break;
                    
                case InFileFailure:
                    /* dialog error - cancel the script */
                    setScriptFile(nil);
                    return [InFileCancel];
                }
            }
        }
    }

    /* 
     *   if we didn't get a result from a script or from the local console,
     *   tell the client UI to display its file dialog 
     */
    if (f == nil)
        f = webMainWin.getInputFile(prompt, dialogType, fileType, flags);

    /* log a synthetic <file> event, if applicable */
    aioLogInputEvent(
        ['<file>',
         f[1] != InFileSuccess ? '' :
         dataType(f[2]) == TypeObject && !f[2].ofKind(FileName) ? 't' :
         f[2]]);
    
    /* return the file information */
    return f;
}

/* ------------------------------------------------------------------------ */
/*
 *   Show an input dialog 
 */
aioInputDialog(icon, prompt, buttons, defaultButton, cancelButton)
{
    /* check for script input */
    local d = nil;
    if (readingEventScript())
    {
        /* we're in a script, so use the regular dialog event reader */
        d = inputDialog(icon, prompt, buttons, defaultButton, cancelButton);

        /* if it failed, forget the result */
        if (d == 0)
            d = nil;
    }

    /* if we didn't get script input, show the dialog via the client UI */
    if (d == nil)
        d = webMainWin.getInputDialog(icon, prompt, buttons,
                                      defaultButton, cancelButton);

    /* log a synthetic <dialog> event, if applicable */
    aioLogInputEvent(['<dialog>', d]);

    /* return the result */
    return d;
}


/* ------------------------------------------------------------------------ */
/*
 *   Set/remove the output logging file
 */
aioSetLogFile(fname, typ = LogTypeTranscript)
{
    /* if there's currently a log file open, close it */
    local log = browserGlobals.logFile;
    if (log != nil)
    {
        switch (browserGlobals.logFileType)
        {
        case LogTypeTranscript:
            /* for a transcript, we have a log console as the handle */
            log.closeConsole();
            break;

        default:
            /* for other types, we have a regular file handle */
            try
            {
                log.closeFile();
            }
            catch (Exception exc)
            {
                /* ignore errors, as we have no way to return them */
            }
            break;
        }

        /* we've closed the handle, so forget it */
        log = nil;
    }

    /* presume success */
    local ok = true;

    /* if there's a filename, create a new console for this file */
    if (fname != nil)
    {
        /* create the output handle according to the type */
        switch (typ)
        {
        case LogTypeTranscript:
            /* 
             *   full transcript - create a log console, which will do the
             *   standard output formatting for us 
             */
            log = new LogConsole(fname, nil, 80);
            break;

        case LogTypeCommand:
        case LogTypeScript:
            /* for other types, create an ordinary text file */
            try
            {
                /* open the log file */
                log = File.openTextFile(fname, FileAccessWrite, nil);

                /* for an event script, write the <eventscript> opener */
                if (typ == LogTypeScript)
                    log.writeFile('<eventscript>\n');
            }
            catch (Exception exc)
            {
                /* if anything went wrong, we have no log file */
                log = nil;
            }
            break;

        default:
            throw RuntimeError.newRuntimeError(2306, 'bad log file type');
        }

        /* we failed if the log handle is nil */
        if (log == nil)
        {
            ok = nil;
            typ = nil;
        }
    }
    else
    {
        /* no longer logging */
        typ = nil;
    }

    /* remember the new handle and log type */
    browserGlobals.logFile = log;
    browserGlobals.logFileType = typ;

    /* return the success/failure indicator */
    return ok;
}

/*
 *   Log an input event.  We call this internally from each of the event
 *   input routines to add the event to any event or command log we're
 *   creating. 
 */
aioLogInputEvent(evt)
{
    /* if the system is maintaining its own input log, write it there */
    logInputEvent(evt);

    /* get the script globals */
    local ltyp = browserGlobals.logFileType;
    local log = browserGlobals.logFile;

    /* get the basic event parameters */
    local evtType = evt[1];
    local param = (evt.length() > 1 ? evt[2] : nil);

    /* format the event based on the event type */
    switch (ltyp)
    {
    case LogTypeTranscript:
        /* transcript - echo command line input */
        if (evt[1] == InEvtLine)
            log.writeToStream(evt[2].htmlify() + '\n');
        break;

    case LogTypeCommand:
        /* command script - write command inputs only */
        if (evt[1] == InEvtLine)
            log.writeFile('>' + evt[2] + '\n');
        break;

    case LogTypeScript:
        /* event script - write all event types */
        switch (evtType)
        {
        case InEvtKey:
            log.writeFile('<key>' + evtCharForScript(param));
            break;
            
        case InEvtTimeout:
            log.writeFile('<timeout>' + param);
            break;
            
        case InEvtHref:
            log.writeFile('<href>' + param);
            break;
            
        case InEvtNoTimeout:
            log.writeFile('<notimeout>');
            break;
            
        case InEvtEof:
            log.writeFile('<eof>');
            break;
            
        case InEvtLine:
            log.writeFile('<line>' + param);
            break;
            
        case InEvtSysCommand:
            log.writeFile('<command>' + param);
            break;
            
        case InEvtEndQuietScript:
            log.writeFile('<endqs>');
            break;

        default:
            /* if it's a string value, it's the literal event tag */
            if (dataType(evtType) == TypeSString)
                log.writeFile(evtType + param);
            break;
        }

        /* add a newline at the end of the event line */
        log.writeFile('\n');
        break;
    }

    /* 
     *   return the event, so that the caller can conveniently return it
     *   after logging it 
     */
    return evt;
}

/* 
 *   Get an InEvtKey event parameter in suitable format for script file
 *   output.  This returns the key as it appears in the event, except that
 *   ASCII control characters are translated to '[ctrl-X]'.  
 */
evtCharForScript(c)
{
    if (c.toUnicode(1) < 32)
    {
        /* it's a control character - return the [ctrl-X] sequence */
        return '[ctrl-<<makeString(c.toUnicode(1) + 64)>>]';
    }
    else
    {
        /* return everything else as it appears in the event descriptor */
        return c;
    }
}


/* ------------------------------------------------------------------------ */
/* 
 *   Generate a string to show hyperlinked text.  The browser UI is always
 *   in HTML mode, so we unconditionally generate the hyperlink.
 *   
 *   If the display text is included, we'll generate the entire link,
 *   including the <A HREF> tag, the hyperlinked text contents, and the
 *   </A> end tag.  If the text is omitted, we'll simply generate the <A
 *   HREF> tag itself, leaving it to the caller to display the text and the
 *   </A>.
 *   
 *   The optional 'flags' is a combination of AHREF_xxx flags indicating
 *   any special properties of the hyperlink.  
 */
aHref(href, txt?, title?, flags = 0)
{
    /* figure extra properties, based on the flags */
    local props = '';
    if (flags & AHREF_Plain)
        props += 'class="plain" ';

    /* generate the <A HREF>, text, and </A>, as applicable */
    return '<a <<props>> href="<<href.findReplace('"', '%22')>>"<<
           (title != nil
            ? ' title="' + title.findReplace('"', '&#34;') + '"'
            : '')
           >> onclick="javascript:return gamehref(event,\'<<
           href.findReplace(['\'', '"'],
                            ['\\\'', '\'+String.fromCharCode(34)+\'']) 
           >>\', \'main.command\', this);"><.a><<
           (txt != nil ? txt + '<./a></a>' : '')>>';
}

/* ------------------------------------------------------------------------ */
/* 
 *   Generate a string to show hyperlinked text, with alternate text if
 *   we're not in HTML mode.  The browser UI is always in HTML mode, so we
 *   unconditionally generate the hyperlink.  
 */
aHrefAlt(href, linkedText, altText, title?)
{
    return aHref(href, linkedText, title);
}

/* ------------------------------------------------------------------------ */
/*
 *   The standard main command window. 
 */
transient commandWin: WebCommandWin
;
    

/* ------------------------------------------------------------------------ */
/*
 *   Generate HTML to wrap the left/right portions of the status line.  The
 *   basic status line has three stages: stage 0 precedes the left portion,
 *   stage 1 comes between the left and right portions, and stage 2 follows
 *   the right portion.  If we're listing exits, we get two more stages:
 *   stage 3 precedes the exit listing, stage 4 follows it.  
 */
statusHTML(stage)
{
    switch(stage)
    {
    case 0:
        /* start the left-aligned portion */
        return '<div class="statusleft">';

    case 1:
        /* close the left portion, and start the right-aligned portion */
        return '</div><div class="statusright">';

    case 2:
        /* 
         *   Close the right portion, and break clear of the floating
         *   sections.  The break is necessary to make sure that the
         *   contents of the two sections count in the window height; some
         *   browsers don't include floating boxes in the content height,
         *   so we need to manually extend the main vertical box's height
         *   past the floating sections. 
         */
        return '</div><div class="statusStrut"></div>';

    case 3:
    case 4:
        /* before/after exit listing - we have nothing to add here */
        return '';

    default:
        return '';
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Web Banner Window.  This is designed as a *partial* drop-in
 *   replacement for the BannerWindow class, using Web UI windows as
 *   implemented in the core TADS javascript client.
 *   
 *   This class is designed to be mixed with a WebWindow subclass.
 *   
 *   This isn't a complete replacement for BannerWindow, because the layout
 *   model for the Web UI is different from the banner window model (the
 *   Web UI model is better and more flexible).  This class implements the
 *   parts of the BannerWindow API related to the stream-oriented output to
 *   the window, so you shouldn't have to change anything that writes HTML
 *   text to the window.  However, you will have to rework code that sets
 *   up the window's layout to use the Web UI model.  
 */
class WebBannerWin: OutputStreamWindow
    /* 
     *   Initialize.  Call this when first displaying the window in the UI.
     */
    init()
    {
        /* set up our output stream */
        createOutputStream();
    }

    /* create our output stream subclass */
    createOutputStreamObj()
    {
        return new transient WebWinOutputStream(self);
    }

    /* flush output */
    flushBanner()
    {
        flushWin();
    }

    /* write text */
    writeToBanner(txt)
    {
        outputStream_.writeToStream(txt);
    }

    /*
     *   Banner window size settings.  We simply ignore these; callers must
     *   rework their layout logic for the Web UI, since the javascript
     *   layout system is so different.  
     */
    setSize(siz, units, advisory) { }
    sizeToContents() { }
;

/*
 *   Output stream for web banner windows
 */
class WebWinOutputStream: OutputStream
    /* construct */
    construct(win)
    {
        /* do the base class construction */
        inherited();
        
        /* save our window */
        win_ = win;
    }

    /* ignore preinit - we're always created dynamically */
    execute() { }

    /* write to the underlying window */
    writeFromStream(txt)
    {
        /* add the text to the window */
        win_.write(txt);
    }

    /* our status line window */
    win_ = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   The basic status line window.  The "banner" in the name is historical,
 *   because the traditional console UI implements the status line as a
 *   banner window.  We don't actually have banner windows in the Web UI;
 *   we use iframes instead.  But we keep the name to make it easier to
 *   port games written for the traditional UI to the Web UI.  
 */
transient statuslineBanner: WebStatusWin, WebBannerWin
;

Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1