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