# settings.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library - settings file management
     *
     *   This is a framework that the library uses to keep track of certain
     *   preference settings - things like the NOTIFY, FOOTNOTES, and EXITS
     *   settings.
     *
     *   The point of this framework is "global" settings - settings that apply
     *   not just to a particular game, but to all games that have a particular
     *   feature.  Things like NOTIFY, FOOTNOTES, and some other such features
     *   are part of the standard library, so they tend to be available in most
     *   games.  Furthermore, they tend to work more or less the same way in
     *   most games.  As a result, a given player will probably prefer to set
     *   the options a particular way for most or all games.  If a player
     *   doesn't like score notification, she'll probably dislike it across the
     *   board, not just in certain games.
     *
     *   This module provides the internal, programmatic core for managing
     *   global preferences.  There's no UI in this part of the implementation;
     *   the adv3 library layers the UI on top via the settingsUI object, but
     *   other alternative UIs could be built using the API provided here.
     *
     *   The framework is extensible - there's an easy, structured way for
     *   library extensions and games to add their own configuration variables
     *   that will be automatically managed by the framework.  All you have to
     *   do to create a new configuration variable is to create a SettingsItem
     *   object to represent it.  Once you've created the object, the library
     *   will automatically find it and manage it for you.
     *
     *   This module is designed to be separable from the adv3 library, so that
     *   alternative libraries or stand-alone (non-library-based) games can
     *   reuse it.  This file has no dependencies on anything in adv3 (at
     *   least, it shouldn't).
     */

    #include <tads.h>
    #include <file.h>

    /* ------------------------------------------------------------------------ */
    /*
     *   A settings item.  This encapsulates a single setting variable.  When
     *   we're saving or restoring default settings, we'll simply loop over all
     *   objects of this class to get or set the current settings.
     *
     *   Note that we don't make any assumptions in this base class about the
     *   type of the value associated with this setting, how it's stored, or
     *   how it's represented in the external configuration file.  This means
     *   that each subclass has to provide the property or properties that
     *   store the item's value, and must also define the methods that operate
     *   on the value.
     *
     *   If you want to force a particular default setting for a particular
     *   preference item, overriding the setting stored in the global
     *   preferences file, you can override that SettingsItem's
     *   settingFromText() method.  This is the method that interprets the
     *   information in the preferences file, so if you want to ignore the
     *   preferences file setting, override this method to set the hard-coded
     *   value of your choosing.
     */
    class SettingsItem: object
        /*
         *   The setting's identifier string.  This is the ID of the setting as
         *   it appears in the external configuration file.
         *
         *   The ID should be chosen to ensure uniqueness.  To reduce the
         *   chances of name collisions, we suggest a convention of using a two
         *   part name: a prefix identifying the source of the name (an
         *   abbreviated version of the name of the library, library extension,
         *   or game), followed by a period as a separator, followed by a short
         *   descriptive name for the variable.  The library follows this
         *   convention by using names of the form "adv3.xxx" - the "adv3"
         *   prefix indicates the standard library.
         *
         *   The ID should contain only letters, numbers, and periods.  Don't
         *   use spaces or punctuation marks (other than periods).
         *
         *   Note that the ID string is for the program's use, not the
         *   player's, so this isn't something we translate to different
         *   languages.  Note, though, that the configuration file is a simple
         *   text file, so it wouldn't hurt to use a reasonably meaningful
         *   name, in case the user takes it upon herself to look at the
         *   contents of the file.
         */
        settingID = ''

        /*
         *   Display a message fragment that shows the current setting value.
         *   We use this to show the player exactly what we're saving or
         *   restoring in response to a SAVE DEFAULTS or RESTORE DEFAULTS
         *   command, so that there's no confusion about which settings are
         *   included.  In most cases, the best thing to show here is the
         *   command that selects the current setting: "NOTIFY ON," for
         *   example.  This is for the UI's convenience; it's not used by the
         *   settings manager itself.
         */
        settingDesc = ""

        /*
         *   Should this item be included in listings shown to the user?  If
         *   this is true, the UI will include this setting in a display list
         *   of current settings shown to the user on request, by calling our
         *   settingDesc method.
         */
        includeInListing = true

        /*
         *   Get the textual representation of the setting - returns a string
         *   representing the setting as it should appear in the external
         *   configuration file.  We use this to write the setting to the file.
         *
         *   Note that this is only needed if the default saveItem() method is
         *   used.
         */
        settingToText() { /* subclasses must override */ }

        /*
         *   Set the current value to the contents of the given string.  The
         *   string contains a textual representation of a setting value, as
         *   previously generated with settingToText().
         *
         *   This is only needed if the default restoreItem() method is used.
         */
        settingFromText(str) { /* subclasses must override */ }

        /*
         *   Load from a settings file.  By default, this simply calls the
         *   setting file object to load the data.
         *
         *   This implementation is suitable for any scalar type, so this won't
         *   need to be overwritten for subclasses that only need to load a
         *   single string value from the file.  Subclasses that implement
         *   complex (non-scalar) datatypes can override this as needed to read
         *   multiple line items from the file.
         */
        restoreItem(s)
        {
            /* look up the file item by ID */
            local fileItem = s.getItem(settingID);

            /*
             *   if this item appears in the file, retrieve its value; if not,
             *   restore my factory default setting
             */
            settingFromText(fileItem != nil ? fileItem.val_ : factoryDefault);
        }

        /*
         *   Save to a settings file.  By default, this makes a string out of
         *   our value and updates or adds our corresponding entry in the file.
         *
         *   This implementation is suitable for any scalar type, so this won't
         *   need to be overwritten for subclasses that only need to store a
         *   single string value in the file.  Subclasses that implement
         *   complex (non-scalar) datatypes can override this as needed to
         *   manipulate multiple line items in the file.
         */
        saveItem(s)
        {
            /* get the string representation of my value */
            local val = settingToText();

            /* add or replace it in the file */
            s.setItem(settingID, val);
        }

        /*
         *   My "factory default" setting.  At pre-init time, before we've
         *   loaded the settings file for the first time, we'll run through all
         *   SettingsItems and store their pre-defined source-code settings
         *   here, as though we were saving the values to a file.  Later, when
         *   we load a file, if we find the file lacks an entry for this
         *   setting item, we'll simply re-load the factory default from this
         *   property.
         */
        factoryDefault = nil
    ;

    /*
     *   A binary settings item - this is for variables that have simple
     *   true/nil values.
     */
    class BinarySettingsItem: SettingsItem
        /* convert to text - use ON or OFF as the representation */
        settingToText() { return isOn ? 'on' : 'off'; }

        /* parse text */
        settingFromText(str)
        {
            /* convert to lower-case and strip off spaces */
            if (rexMatch('<space>*(<alpha>+)', str.toLower()) != nil)
                str = rexGroup(1)[3];

            /* get the new setting */
            isOn = (str.toLower() == 'on');
        }

        /* our value is true (on) or nil (off) */
        isOn = nil
    ;

    /*
     *   A string settings item.  This is for variables that have scalar string
     *   values.  Value strings can contain anything except newlines.
     */
    class StringSettingsItem: SettingsItem
        /* convert to text */
        settingToText()
        {
            /* quote the value if necessary */
            return quoteValue(val);
        }

        /* parse text */
        settingFromText(str)
        {
            /*
             *   If the value isn't quoted, use the value as-is, trimming off
             *   leading and trailing spaces.  If it at least starts with a
             *   quote, remove the leading and trailing quote (if present) and
             *   translate backslash sequences.
             */
            if (rexMatch('^<space>*"', str))
            {
                /* it's quoted - remove the quotes and translate backslashes */
                val = rexReplace(
                    [leadTrailSpPat, '\\"', '\\\\', '\\n', '\\r'], str,
                    ['', '"', '\\', '\n', '\r']);
            }
            else
            {
                /* no leading quote; just trim spaces */
                rexMatch(trimSpPat, str);
                val = rexGroup(1)[3];
            }
        }

        leadTrailSpPat = static new RexPattern('^<space>+|<space>+$')
        trimSpPat = static new RexPattern('^<space>*(.*?)<space>*$')

        /*
         *   Class method: quote a string value for storing in the file.  If
         *   the string has any leading or trailing spaces, starts with a
         *   double quote, or contains any newlines, we'll quote it; otherwise
         *   we'll return it as-is.
         */
        quoteValue(str)
        {
            /*
             *   if the value needs quoting, quote it, otherwise just return it
             *   unchanged
             */
            if (rexSearch(needQuotePat, str))
            {
                /*
                 *   add quotes around the string, and backslash-escape any
                 *   quotes, backslashes, or newlines within the string
                 */
                return '"' + val.findReplace(
                    ['"', '\\', '\n', '\r'], ['\\"', '\\\\', '\\n', '\\r'])
                    + '"';
            }
            else
            {
                /* quotes aren't needed*/
                return str;
            }
        }

        needQuotePat = static new RexPattern('^<space>+|^"|[\r\n]')

        /* our current value string */
        val = ''
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   The settings manager.  This object gathers up some global methods for
     *   managing the saved settings.  This base class provides only a
     *   programmatic interface - it doesn't have a user interface.
     */
    settingsManager: object
        /*
         *   Save the current settings.  This writes out the current settings
         *   to the global settings file.
         *
         *   On any error, the method throws an exception.  Possible errors
         *   include:
         *
         *   - FileCreationException indicates that the settings file couldn't
         *   be opened for writing.
         */
        saveSettings()
        {
            /* retrieve the current settings */
            local s = retrieveSettings();

            /* if that failed, there's nothing more we can do */
            if (s == nil)
                return;

            /*
             *   Update the file's contents with all of the current in-memory
             *   settings objects (applying the filter condition, if provided).
             */
            forEachInstance(SettingsItem, { item: item.saveItem(s) });

            /* write out the settings */
            storeSettings(s);
        }

        /*
         *   Restore all of the settings.  If an error occurs, we'll throw an
         *   exception:
         *
         *   - SettingsNotSupportedException - this is an older interpreter
         *   that doesn't support the "special files" feature, so we can't save
         *   or restore the default settings.
         */
        restoreSettings()
        {
            /* retrieve the current settings */
            local s = retrieveSettings();

            /*
             *   update all of the in-memory settings objects with the values
             *   from the file
             */
            forEachInstance(SettingsItem, {item: item.restoreItem(s)});
        }

        /*
         *   Retrieve the settings from the global settings file.  This returns
         *   a SettingsFileData object that describes the file's contents.
         *   Note that if there simply isn't an existing settings file, we'll
         *   successfully return a SettingsFileData object with no data - the
         *   absence of a settings file isn't an error, but is merely
         *   equivalent to an empty settings file.
         */
        retrieveSettings()
        {
            local f;
            local s = new SettingsFileData();
            local linePat = new RexPattern(
                '<space>*(<alphanum|-|_|$|lsquare|rsquare|percent|dot>+)'
                + '<space>*=<space>*([^\n]*)\n?$');

            /*
             *   Try opening the settings file.  Older interpreters don't
             *   support the "special files" feature; if the interpreter
             *   predates special file support, it'll throw a "string value
             *   required," since it won't recognize the special file ID value
             *   as a valid filename.
             */
            try
            {
                /* open the "library defaults" special file */
                f = File.openTextFile(LibraryDefaultsFile, FileAccessRead);
            }
            catch (FileNotFoundException fnf)
            {
                /*
                 *   The interpreter supports the special file, but the file
                 *   doesn't seem to exist.  Simply return the empty file
                 *   contents object.
                 */
                return s;
            }
            catch (RuntimeError rte)
            {
                /*
                 *   if the error is "string value required," then we have an
                 *   older interpreter that doesn't support special files -
                 *   indicate this by returning nil
                 */
                if (rte.errno_ == 2019)
                {
                    /* re-throw this as a SettingsNotSupportedException */
                    throw new SettingsNotSupportedException();
                }

                /* other exceptions are unexpected, so re-throw them */
                throw rte;
            }

            /* read the file */
            for (;;)
            {
                local l;

                /* read the next line */
                l = f.readFile();

                /* stop if we've reached end of file */
                if (l == nil)
                    break;

                /* parse the line */
                if (rexMatch(linePat, l) != nil)
                {
                    /*
                     *   it parsed - add the variable and its value to the
                     *   contents object
                     */
                    s.addItem(rexGroup(1)[3], rexGroup(2)[3]);
                }
                else
                {
                    /* it doesn't parse, so just keep the line as a comment */
                    s.addComment(l);
                }
            }

            /* done with the file - close it */
            f.closeFile();

            /* return the populated file contents object */
            return s;
        }

        /* store the given SettingsFileData to the global settings file */
        storeSettings(s)
        {
            /*
             *   Open the "library defaults" file.  Note that we don't have to
             *   worry here about the old-interpreter situation that we handle
             *   in retrieveSettings() - if the interpreter doesn't support
             *   special files, we won't ever get this far, because we always
             *   have to retrieve the current file's contents before we can
             *   store the new contents.
             */
            local f = File.openTextFile(LibraryDefaultsFile, FileAccessWrite);

            /* write each line of the file's contents */
            foreach (local item in s.lst_)
                item.writeToFile(f);

            /* done with the file - close it */
            f.closeFile();
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Exception: the settings file mechanism isn't supported on this
     *   interpreter.  This indicates that this is an older interpreter that
     *   doesn't support the "special files" feature, so we can't save or load
     *   the global settings file.
     */
    class SettingsNotSupportedException: Exception
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   SettingsFileData - this is an object we use to represent the contents
     *   of the configuration file.
     */
    class SettingsFileData: object
        construct()
        {
            /*
             *   We store the contents of the file in two ways: as a list, in
             *   the same order in which the contents appear in the file; and
             *   as a lookup table keyed by variable name.  The list lets us
             *   preserve the parts of the file's contents that we don't need
             *   to change when we read it in and write it back out.  The
             *   lookup table makes it easy to look up particular variable
             *   values.
             */
            tab_ = new LookupTable(16, 32);
            lst_ = new Vector(16);
        }

        /*
         *   find an item - returns a SettinsFileItem for the key, or nil if
         *   there's no existing item
         */
        getItem(id)
        {
            /* return the entry from our ID-keyed table */
            return tab_[id];
        }

        /* iterate over all data (non-comment) items in the file */
        forEach(func)
        {
            /* scan the list */
            foreach (local s in lst_)
            {
                /* if this is a data item, pass it to the callback */
                if (s.ofKind(SettingsFileItem))
                    func(s.id_, s.val_);
            }
        }

        /* add a variable */
        addItem(id, val)
        {
            local item;

            /* create the item descriptor object */
            item = new SettingsFileItem(id, val);

            /* append it to our file-contents-ordered list */
            lst_.append(item);

            /* add it to the lookup table, keyed by the variable ID */
            tab_[id] = item;
        }

        /* set a variable, adding a new variable if it doesn't already exist */
        setItem(id, val)
        {
            /* look for an existing SettingsFileItem entry for my ID */
            local fileItem = getItem(id);

            /* update the item if it exists, otherwise add a new one */
            if (fileItem != nil)
                fileItem.val_ = val;
            else
                addItem(id, val);
        }

        /* add a comment line */
        addComment(str)
        {
            /* append a comment descriptor to the contents list */
            lst_.append(new SettingsFileComment(str));
        }

        /* delete an item */
        delItem(id)
        {
            /* if it's in our table, delete it */
            local item = tab_[id];
            if (item != nil)
            {
                /* delete it from the lookup table */
                tab_.removeElement(id);

                /* delete it from the file source list */
                lst_.removeElement(item);
            }
        }

        /* lookup table of values, keyed by variable name */
        tab_ = nil

        /* a list of SettingsFileItem objects giving the contents of the file */
        lst_ = nil
    ;

    /*
     *   SettingsFileItem - this object describes a single item within an
     *   external settings file.
     */
    class SettingsFileItem: object
        construct(id, val)
        {
            id_ = id;
            val_ = val;
        }

        /* write this value to a file */
        writeToFile(f) { f.writeFile(id_ + ' = ' + val_ + '\n'); }

        /* the variable's ID */
        id_ = nil

        /* the string representation of the value */
        val_ = nil
    ;

    /*
     *   SettingsFileComment - this object describes an unparsed line in the
     *   settings file.  We treat lines that don't match our parsing rules as
     *   comments.  We preserve the contents and order of these lines, but we
     *   don't otherwise try to interpret them.
     */
    class SettingsFileComment: object
        construct(str)
        {
            /* if it doesn't end in a newline, add a newline */
            if (!str.endsWith('\n'))
                str += '\n';

            /* remember the string */
            str_ = str;
        }

        /* write the comment line to a file */
        writeToFile(f) { f.writeFile(str_); }

        /* the text from the file */
        str_ = nil
    ;

TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3