Mercurial > projects > mde
view mde/lookup/Options.d @ 64:cc3763817b8a
Overhauled Options so that it now uses templates and mixins for type-specific internals, and supported types can be adjusted via just one list.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sun, 29 Jun 2008 11:55:55 +0100 |
parents | 66d555da083e |
children | 108d123238c0 |
line wrap: on
line source
/* LICENSE BLOCK Part of mde: a Modular D game-oriented Engine Copyright © 2007-2008 Diggory Hardy 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 2 of the License, 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, see <http://www.gnu.org/licenses/>. */ /** This module handles stored options, currently all except input maps. * * The purpose of having all options centrally controlled is to allow generic handling by the GUI * and ease saving and loading of values. The Options class is only really designed around handling * small numbers of variables for now. */ module mde.lookup.Options; import mde.exception; import mde.mergetag.Reader; import mde.mergetag.Writer; import mde.mergetag.DataSet; import mde.mergetag.exception; import mde.setup.paths; import tango.scrapple.text.convert.parseTo : parseTo; import tango.scrapple.text.convert.parseFrom : parseFrom; import tango.core.Exception : ArrayBoundsException; import tango.util.log.Log : Log, Logger; /** Base class for handling options. * * This class itself handles no options and should not be instantiated, but provides a sub-classable * base for generic options handling. Also, the static portion of this class tracks sub-class * instances and provides loading and saving methods. * * Each sub-class provides named variables for maximal-speed reading. Local sub-class references * should be used for reading variables, and via the addOptionsClass() hook will be loaded from * files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will * not be saved; instead use set(), for example, miscOpts.set!(char[])("L10n","en-GB"). Use an * example like OptionsMisc as a template for creating a new Options sub-class. * * Details: Options sub-classes hold associative arrays of pointers to all option variables, with a * char[] id. This list is used for saving, loading and to provide generic GUI options screens. The * built-in support in Options is only for bool, int and char[] types (a float type may get added). * Further to this, a generic class is used to store all options which have been changed, and if any * have been changed, is merged with options from the user conf dir and saved on exit. */ class Options : IDataSection { // All supported types, for generic handling via templates. It should be possible to change // the supported types simply by changing this list now (untested). template store(A...) { alias A store; } alias store!(bool, int, double, char[]) TYPES; //BEGIN Templates: internal private { // Get name of a type. Basically just stringof, but special handling for arrays. // Use TName!(T) for a valid symbol name, and T.stringof for a type. template TName(T : T[]) { const char[] TName = TName!(T) ~ "A"; } template TName(T) { const char[] TName = T.stringof; } // Pointer lists template PLists(T, A...) { static if (A.length) { const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n" ~ PLists!(A); } else const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n"; } // True if type is one of A template TIsIn(T, A...) { static if (A.length) { static if (is(T == A[0])) const bool TIsIn = true; else const bool TIsIn = TIsIn!(T,A[1..$]); } else const bool TIsIn = false; // no more possibilities } // For addTag template addTagMixin(T, A...) { const char[] ifBlock = `if (tp == "`~T.stringof~`") { `~T.stringof~`** p = id in opts`~TName!(T)~`; if (p !is null) **p = parseTo!(`~T.stringof~`) (dt); }`; static if (A.length) const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin; else const char[] addTagMixin = ifBlock; } // For writeAll template writeAllMixin(A...) { static if (A.length) { const char[] writeAllMixin = `foreach (ID id, `~A[0].stringof~`* val; opts`~TName!(A[0])~`) dlg ("`~A[0].stringof~`", id, parseFrom!(`~A[0].stringof~`) (*val));` ~ writeAllMixin!(A[1..$]); } else const char[] writeAllMixin = ``; } } //END Templates: internal //BEGIN Static static { /** Add an options sub-class to the list for loading and saving. * * Call from static this() (before Init calls load()). */ void addOptionsClass (Options c, char[] i) in { // Trap a couple of potential coding errors: assert (c !is null); // Instance must be created before calling addOptionsClass assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement } body { subClasses[cast(ID) i] = c; } // Track all sections for saving/loading/other generic handling. Options[ID] subClasses; bool changed = false; // any changes at all, i.e. do we need to save? /* Load/save options from file. * * If the file doesn't exist, no reading is attempted (options are left at default values). */ private const fileName = "options"; private const MT_LOAD_EXC = "Loading options aborted:"; void load () { // Check it exists (if not it should still be created on exit). // Don't bother checking it's not a folder, because it could still be a block or something. if (!confDir.exists (fileName)) return; try { IReader reader; reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH); reader.dataSecCreator = delegate IDataSection(ID id) { /* Recognise each defined section, and return null for unrecognised sections. */ Options* p = id in subClasses; if (p !is null) return *p; else return null; }; reader.read; } catch (MTException e) { logger.fatal (MT_LOAD_EXC); logger.fatal (e.msg); throw new optionsLoadException ("Mergetag exception (see above message)"); } } void save () { if (!changed) return; // no changes to save DataSet ds = new DataSet(); foreach (id, subOpts; subClasses) ds.sec[id] = subOpts.optionChanges; // Read locally-stored options try { IReader reader; reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds); reader.dataSecCreator = delegate IDataSection(ID id) { return null; // All recognised sections are already in the dataset. }; reader.read; } catch (MTFileIOException) { // File either didn't exist or couldn't be opened. // Presuming the former, this is not a problem. } catch (MTException e) { // Log a message and continue, overwriting the file: logger.error (MT_LOAD_EXC); logger.error (e.msg); } try { IWriter writer; writer = confDir.makeMTWriter (fileName, ds); writer.write(); } catch (MTException e) { logger.error ("Saving options aborted! Reason:"); logger.error (e.msg); } } private Logger logger; static this() { logger = Log.getLogger ("mde.options"); } } //END Static //BEGIN Non-static /** Set option symbol of an Options sub-class to val. * * Due to the way options are handled generically, string IDs must be used to access the options * via hash-maps, which is a little slower than direct access but necessary since the option * must be changed in two separate places. */ void set(T) (char[] symbol, T val) { static if (!TIsIn!(T,TYPES)) static assert (false, "Options.set does not currently support type "~T.stringof); mixin (`alias opts`~T.stringof~` optsVars;`); changed = true; // something got set (don't bother checking this isn't what it already was) try { *(optsVars[cast(ID) symbol]) = val; optionChanges.set!(T) (cast(ID) symbol, val); } catch (ArrayBoundsException) { // log and ignore: logger.error ("Options.set: unkw!"); } } protected { OptionChanges optionChanges; // all changes to options (for saving) // The "pointer lists": mixin (PLists!(TYPES)); } //BEGIN Mergetag loading/saving code void addTag (char[] tp, ID id, char[] dt) { mixin(addTagMixin!(TYPES).addTagMixin); } void writeAll (ItemDelg dlg) { mixin(writeAllMixin!(TYPES)); } //END Mergetag loading/saving code //END Non-static //BEGIN Templates: impl & optionsThis private { // Return index of first comma, or halts if not found. template cIndex(char[] A) { static if (A.length == 0) static assert (false, "Error in implementation"); else static if (A[0] == ',') const size_t cIndex = 0; else const size_t cIndex = 1 + cIndex!(A[1..$]); } // Return index of first semi-colon, or halts if not found. template scIndex(char[] A) { static if (A.length == 0) static assert (false, "Error: no trailing semi-colon"); else static if (A[0] == ';') const size_t scIndex = 0; else const size_t scIndex = 1 + scIndex!(A[1..$]); } // Look for "type symbols;" in A and return symbols as a comma separated list of names // (even if type is encountered more than once). Output may contain spaces and, if // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$. template parseT(char[] type, char[] A) { static if (A.length < type.length + 1) // end of input, no match const char[] parseT = ""; else static if (A[0] == ' ') // leading whitespace: skip const char[] parseT = parseT!(type, A[1..$]); else static if (A[0..type.length] == type && A[type.length] == ' ') // match const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~ parseT!(type, A[scIndex!(A)+1 .. $]); else // no match const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]); } // May have a trailing comma. Assumes cIndex always returns less than A.$. template aaVars(char[] A) { static if (A.length == 0) const char[] aaVars = ""; else static if (A[0] == ' ') const char[] aaVars = aaVars!(A[1..$]); else const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~ aaVars!(A[cIndex!(A)+1..$]); } // strip Trailing Comma template sTC(char[] A) { static if (A.length && A[$-1] == ',') const char[] sTC = A[0..$-1]; else const char[] sTC = A; } // if string is empty (other than space) return null, otherwise enclose: [A] template listOrNull(char[] A) { static if (A.length == 0) const char[] listOrNull = "null"; else static if (A[0] == ' ') const char[] listOrNull = listOrNull!(A[1..$]); else const char[] listOrNull = "["~A~"]"; } // for recursing on TYPES template optionsThisInternal(char[] A, B...) { static if (B.length) { const char[] optionsThisInternal = `opts`~TName!(B[0])~` = `~listOrNull!(sTC!(aaVars!(parseT!(B[0].stringof,A))))~";\n" ~ optionsThisInternal!(A,B[1..$]); } else const char[] optionsThisInternal = ``; } } protected { /** Produces the implementation code to go in the constuctor. */ template optionsThis(char[] A) { const char[] optionsThis = "optionChanges = new OptionChanges;\n" ~ optionsThisInternal!(A,TYPES); } /+ Needs too many custom parameters to be worth it? Plus makes class less readable. /** Produces the implementation code to go in the static constuctor. */ template optClassAdd(char[] symb) { const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n"; }+/ /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]") * * Where type is one of bool, int, double, char[]. E.g. * --- * mixin (impl ("bool a, b; int i;")); * --- * * In case this() needs to be customized, mixin(impl!(A)) is equivalent to: * --- * mixin (A~` this () { `~optionsThis!(A)~` }`); * --- * * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing * semi-colon (;) or you'll get told off! :D * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if * necessary. * * Extending: mixins could also be used for the static this() {...} or even the whole * class, but doing so would rather decrease readability of any implementation. */ template impl(char[] A /+, char[] symb+/) { const char[] impl = A~"\nthis(){\n"~optionsThis!(A)~"}"; // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}" } } //END Templates: impl & optionsThis } /* Special class to store all locally changed options, whatever the section. */ class OptionChanges : Options { //BEGIN Templates private { template Vars(A...) { static if (A.length) { const char[] Vars = A[0].stringof~`[] `~TName!(A[0])~`s;` ~ Vars!(A[1..$]); } else const char[] Vars = ``; } // For addTag; different to Options.addTag(). // Reverse priority: only load symbols not currently existing template addTagMixin(T, A...) { const char[] ifBlock = `if (tp == "`~T.stringof~`") { if ((id in opts`~TName!(T)~`) is null) { `~TName!(T)~`s ~= parseTo!(`~T.stringof~`) (dt); opts`~TName!(T)~`[id] = &`~TName!(T)~`s[$-1]; } }`; static if (A.length) const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin; else const char[] addTagMixin = ifBlock; } } //END Templates // These store the actual values, but are never accessed directly except when initially added. // optsX store pointers to each item added along with the ID and are used for access. mixin(Vars!(TYPES)); this () {} void set(T) (ID id, T x) { static if (!TIsIn!(T,TYPES)) static assert (false, "OptionChanges.set does not currently support type "~T.stringof); mixin (`alias opts`~T.stringof~` optsVars;`); mixin (`alias `~T.stringof~`s vars;`); T** p = id in optsVars; if (p !is null) **p = x; else { vars ~= x; optsVars[id] = &vars[$-1]; } } //BEGIN Mergetag loading/saving code // Reverse priority: only load symbols not currently existing void addTag (char[] tp, ID id, char[] dt) { mixin (addTagMixin!(TYPES).addTagMixin); } //END Mergetag loading/saving code } /* NOTE: Options sub-classes are expected to use a template to ease inserting contents and * hide some of the "backend" functionality. Use impl as below, or read the documentation for impl. * * Each entry should have a Translation entry with humanized names and descriptions in * data/L10n/ClassName.mtt * * To create a new Options sub-class, just copy, paste and adjust. */ /** A home for all miscellaneous options, at least for now. */ OptionsMisc miscOpts; class OptionsMisc : Options { mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;")); static this() { miscOpts = new OptionsMisc; Options.addOptionsClass (miscOpts, "misc"); } }