changeset 105:08651e8a8c51

Quit button, big changes to content system. Moved mde.gui.content to mde.content to reflect it's not only used by the gui. Split Content module into Content and AStringContent. New AContent and EventContent class. Callbacks are now generic and implemented in AContent. Renamed TextContent to StringContent and ValueContent to AStringContent.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 29 Nov 2008 12:36:39 +0000
parents ee209602770d
children 7f7b40fed72b
files codeDoc/todo.txt data/conf/gui.mtt mde/content/AStringContent.d mde/content/Content.d mde/content/Items.d mde/content/Services.d mde/gui/WidgetManager.d mde/gui/content/Content.d mde/gui/content/Items.d mde/gui/content/Services.d mde/gui/widget/Ifaces.d mde/gui/widget/TextWidget.d mde/gui/widget/createWidget.d mde/gui/widget/layout.d mde/gui/widget/miscContent.d mde/gui/widget/textContent.d mde/imde.d mde/lookup/Options.d
diffstat 18 files changed, 648 insertions(+), 625 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/todo.txt	Wed Nov 26 13:07:46 2008 +0000
+++ b/codeDoc/todo.txt	Sat Nov 29 12:36:39 2008 +0000
@@ -2,21 +2,6 @@
 License: GNU General Public License version 2 or later (see COPYING)
 
 
-Content:
-Need a way for a single content to contain multiple "sub-contents", and a way for widgets to be bound to a sub-content.
-->  how
-    ->  Use indecies; widget's use an integer value to choose index (or a string)?
-    ->  Protection: read-only and read-write contents?
-->  why
-    ->  Dynamic lists of name, value and possibly description fields.
-        ->  Done in a basic case, so is it needed?
-Generic Content creation:
-->  Created centrally, e.g. from options or translation strings.
-    ->  Creator keeps a hash-map of all created content, so that if the same content is asked for again the same content object will be returned, not a new object (besides efficiency, this keeps content synchronized).
-->  Generic ContentList type for instancing on at run-time.
-->  Items in ContentList may represent a cluster of items; e.g. an option plus it's name and description. These may be special classes, but should use a generic interface allowing getting sub-contents (e.g. value/name/desc.) via an index.
-
-
 GUI:
 ->  Widgets:
 ->  scripted widgets
--- a/data/conf/gui.mtt	Wed Nov 26 13:07:46 2008 +0000
+++ b/data/conf/gui.mtt	Sat Nov 29 12:36:39 2008 +0000
@@ -3,7 +3,7 @@
 <char[]|Design="Working">
 {Working}
 <WidgetData|root={0:[0xC100,0,3,3],1:["square","blank","square","blank","floating","blank","square","blank","square"]}>
-<WidgetData|square={0:[0x10,10,10]}>
+<WidgetData|square={0:[0x1,6,6]}>
 <WidgetData|blank={0:[0x2]}>
 <WidgetData|opts={0:[0x2031,0x6030,0],1:["Options","optCls"]}>
 <WidgetData|optCls={0:[0xC100,0,2,1],1:["optName","optVars"]}>
@@ -17,6 +17,6 @@
 <WidgetData|floating={0:[0x8200,6,14,6],1:["L10n","blank","opts"]}>
 <WidgetData|L10n={0:[0x2031,0xC100,0,2,1],1:["Options.MiscOptions.L10n","optBox2","apply"]}>
 <WidgetData|optBox2={0:[0xC100,0,1,3],1:["optName","optSep","optVal"]}>!{cloned to avoid bug #4}
-<WidgetData|apply={0:[0x11],1:["Apply (unimplemented)"]}>
+<WidgetData|apply={0:[0x2031,0x4043],1:["quit"]}>
 {Basic}
 <WidgetData|root={0:[0x21,0x90D970],1:["A string!"]}>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/AStringContent.d	Sat Nov 29 12:36:39 2008 +0000
@@ -0,0 +1,259 @@
+/* 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/>. */
+
+/** The content system − string-based editable content.
+ */
+module mde.content.AStringContent;
+public import mde.content.Content;
+
+//FIXME: efficient conversions? Need to dup result when formatting a string anyway?
+import Int = tango.text.convert.Integer;
+import Float = tango.text.convert.Float;
+import derelict.sdl.keysym;
+
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+    static this () {
+        logger = Log.getLogger ("mde.content.AStringContent");
+    }
+}
+
+// Used by Options
+template ContentN(T) {
+    static if (is(T == bool)) {
+	const char[] ContentN = "BoolContent";
+    } else static if (is(T == int)) {
+	const char[] ContentN = "IntContent";
+    } else static if (is(T == double)) {
+	const char[] ContentN = "DoubleContent";
+    } else static if (is(T == char[])) {
+	const char[] ContentN = "StringContent";
+    } else
+	static assert (false, "No Content of type "~T.stringof);
+}
+
+/** Base class for content containing a simple value editable as text. All content types used by
+ * Options extend this class.
+ *
+ * All derived classes should have the following functions:
+ * ---
+ *  void endEdit ();	// Should convert sv and assign to self, then call endEvent
+ * // Used by Options:
+ *  BoolContent changeCb (void delegate (char[] symbol,T value) cb);	// The callback used by Options
+ *  void assignNoCb (T val);	// assign val, but without calling callbacks
+ * ---
+ * On any assignation (by this, assignNoCb, opAssign) the value should be converted to a string and
+ * assigned to sv, and pos should be clamped to [0,sv.length] (i.e. enforce pos <= sv.length). */
+abstract class AStringContent : AContent
+{
+    protected this (char[] symbol) {
+	super (symbol);
+    }
+    
+    /// Get the text.
+    char[] toString (uint i) {
+        return i == 0 ? sv
+             : i == 1 ? name_
+             : i == 2 ? desc_
+             : null;
+    }
+    
+    /** Acts on a keystroke and returns the new value.
+     *
+     * Supports one-line editing: left/right, home/end, backspace/delete. */
+    char[] keyStroke (ushort sym, char[] i) {
+	debug assert (i.length, "StringContent.keyStroke: no value (??)");	// impossible?
+	char k = *i;
+	if (k > 0x20) {
+	    if (k == 0x7f) {		// delete
+		size_t p = pos;
+		if (p < sv.length) ++p;
+		while (p < sv.length && (sv[p] & 0x80) && !(sv[p] & 0x40))
+		    ++p;
+		sv = sv[0..pos] ~ sv[p..$];
+	    } else {			// insert character
+		char[] tail = sv[pos..$];
+		sv.length = sv.length + i.length;
+		size_t npos = pos+i.length;
+		if (tail) sv[npos..$] = tail.dup;	// cannot assign with overlapping ranges
+		    sv[pos..npos] = i;
+		pos = npos;
+	    }
+	} else {			// use sym; many keys output 0
+	    if (sym == SDLK_BACKSPACE) {	// backspace; k == 0x8
+		char[] tail = sv[pos..$];
+		if (pos) --pos;
+		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
+		    --pos;
+		sv = sv[0..pos] ~ tail;
+	    } else if (sym == SDLK_LEFT) {
+		if (pos) --pos;
+		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
+		    --pos;
+	    } else if (sym == SDLK_RIGHT) {
+		if (pos < sv.length) ++pos;
+		while (pos < sv.length && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
+		    ++pos;
+	    } else if (sym == SDLK_HOME || sym == SDLK_UP) {
+		pos = 0;
+	    } else if (sym == SDLK_END || sym == SDLK_DOWN) {
+		pos = sv.length;
+	    } else
+		debug logger.trace ("Unhandled symbol: {}", sym);
+	}
+	return sv;
+    }
+    
+    /// Get the character the edit cursor is in front of
+    size_t editIndex () {
+	size_t i = 0;
+	for (size_t p = 0; p < pos; ++p)
+	    if (!(sv[p] & 0x80) || sv[p] & 0x40)
+		++i;
+	return i;
+    }
+    
+    /// Call after editing a string
+    void endEdit ();
+    
+protected:
+    char[] sv;		// string of value; updated on assignment for displaying and editing
+    size_t pos;		// editing position; used by keyStroke
+}
+
+class BoolContent : AStringContent
+{
+    /** Create a content with _symbol name symbol. */
+    this (char[] symbol, bool val = false) {
+	assignNoCb (val);
+	super (symbol);
+    }
+    
+    void assignNoCb (bool val) {
+	v = val;
+	sv = v ? "true" : "false";
+	if (pos > sv.length) pos = sv.length;
+    }
+    void opAssign (bool val) {
+	assignNoCb (val);
+	endEvent;
+    }
+    bool opCall () {
+        return v;
+    }
+    alias opCall opCast;
+    
+    override void endEdit () {
+	v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
+	endEvent;
+    }
+    
+protected:
+    bool v;
+}
+
+/** Text content. */
+class StringContent : AStringContent
+{
+    this (char[] symbol, char[] val = null) {
+        v = val;
+	super (symbol);
+    }
+    
+    void assignNoCb (char[] val) {
+	v = val;
+	if (pos > sv.length) pos = sv.length;
+    }
+    void opAssign (char[] val) {
+	assignNoCb (val);
+	endEvent;
+    }
+    char[] opCall () {
+        return v;
+    }
+    alias opCall opCast;
+    
+    override void endEdit () {
+	endEvent;
+    }
+    
+protected:
+    alias sv v;		// don't need separate v and sv in this case
+}
+
+/** Integer content. */
+class IntContent : AStringContent
+{
+    /** Create a content with _symbol name symbol. */
+    this (char[] symbol, int val = 0) {
+        assignNoCb (val);
+	super (symbol);
+    }
+    
+    void assignNoCb (int val) {
+	v = val;
+	sv = Int.toString (v);
+	if (pos > sv.length) pos = sv.length;
+    }
+    void opAssign (int val) {
+	assignNoCb (val);
+	endEvent;
+    }
+    int opCall () {
+        return v;
+    }
+    alias opCall opCast;
+    
+    override void endEdit () {
+	v = Int.toInt (sv);
+	endEvent;
+    }
+    
+protected:
+    int v;
+}
+
+/** Double content. */
+class DoubleContent : AStringContent
+{
+    /** Create a content with _symbol name symbol. */
+    this (char[] symbol, double val = 0) {
+        assignNoCb (val);
+	super (symbol);
+    }
+    
+    void assignNoCb (double val) {
+	v = val;
+	sv = Float.toString (v);
+	if (pos > sv.length) pos = sv.length;
+    }
+    void opAssign (double val) {
+	assignNoCb (val);
+	endEvent;
+    }
+    double opCall () {
+        return v;
+    }
+    alias opCall opCast;
+    
+    override void endEdit () {
+	v = Float.toFloat (sv);
+	endEvent;
+    }
+    
+protected:
+    double v;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/Content.d	Sat Nov 29 12:36:39 2008 +0000
@@ -0,0 +1,142 @@
+/* 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/>. */
+
+/** The content system − common interface and a few classes without external dependencies.
+ */
+module mde.content.Content;
+
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+    static this () {
+        logger = Log.getLogger ("mde.content.Content");
+    }
+}
+
+/** IContent − interface for all Content classes.
+ *
+ * Services like copy/paste could work on universal content. However, they would need to run a
+ * conversion to the appropriate type (or try next-oldest item on clipboard?). */
+interface IContent
+{
+    /** Generically return strings.
+     *
+     * This serves two purposes: generically returning a string of/related to the content (i == 0),
+     * and returning associated descriptors. Functions should adhere to (or add to) this table.
+     *
+     *  $(TABLE
+     *  $(TR $(TH i) $(TH returns))
+     *  $(TR $(TD 0) $(TD value))
+     *  $(TR $(TD 1) $(TD Translated name or null))
+     *  $(TR $(TD 2) $(TD Translated description or null))
+     *  $(TR $(TD other) $(TD null))
+     *  ) */
+    char[] toString (uint i);
+}
+
+/** The base for $(I most) content classes.
+ *
+ * Includes generic callback support, toString implementation and symbol access.
+ * 
+ * Derived classes should impement:
+ * ---
+ *  this (char[] symbol, T val = /+ default value +/);
+ *  void opAssign (T val);	// assign val, calling callbacks
+ *  T opCall ();		// return value
+ *  alias opCall opCast;
+ * --- */
+class AContent : IContent
+{
+    this (char[] symbol) {
+	this.symbol = symbol;
+    }
+    
+    void name (char[] n, char[] d = null) {
+	name_ = n;
+	desc_ = d;
+    }
+    
+    /// Current implementation has 1 callback; can be changed to allow many.
+    EventContent addCallback (void delegate (AContent) cb) {
+	this.cb = cb;
+	return this;
+    }
+    
+    char[] toString (uint i) {
+	return i == 0 ? "No value"
+	: i == 1 ? name_
+	: i == 2 ? desc_
+	: null;
+    }
+    
+    /// End of an event, e.g. a button release or end of an edit (calls callbacks).
+    void endEvent () {
+	if (cb)
+	    cb (this);
+    }
+    
+    final char[] symbol;	// Symbol name for this content
+protected:
+    char[] name_, desc_;	// name and description
+    void delegate (AContent) cb;
+}
+
+// FIXME: needs changes to allow updating translated strings. Move to Content and extend AContent?
+/** A generic way to handle a list of type IContent. */
+class ContentList : AContent
+{
+    this (char[] symbol, AContent[] list = null) {
+	list_.length = list.length;
+	foreach (i,c; list)
+	    list_[i] = c;
+	super (symbol);
+    }
+    this (char[] symbol, AContent[char[]] l) {
+	list_.length = l.length;
+	size_t i;
+	foreach (c; l)
+	    list_[i++] = c;
+	super (symbol);
+    }
+    
+    IContent[] list () {
+	return list_;
+    }
+    
+protected:
+    final IContent[] list_;
+}
+
+/** Created on errors to display and log a message. */
+class ErrorContent : IContent
+{
+    this (char[] msg) {
+	msg_ = msg;
+    }
+    
+    char[] toString (uint i) {
+	return i == 0 ? msg_
+	     : i == 1 ? "Error"
+	     : null;
+    }
+    
+protected:
+    char[] msg_;
+}
+
+/** A Content with no value but able to pass on an event.
+*
+* The point being that a button can be tied to one of these. */
+alias AContent EventContent;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/Items.d	Sat Nov 29 12:36:39 2008 +0000
@@ -0,0 +1,103 @@
+/* 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/>. */
+
+/**************************************************************************************************
+ * A generic way to access content items.
+ *************************************************************************************************/
+module mde.content.Items;
+
+import mde.content.Content;
+import mde.gui.exception;
+
+import mde.imde;
+import mde.lookup.Options;
+import mde.lookup.Translation;
+
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+    static this () {
+	logger = Log.getLogger ("mde.gui.content.Items");
+    }
+}
+
+    /** Get a specific content item.
+     *
+     * E.g. get ("Options.MiscOptions.L10n") returns miscOpts.L10n,
+     * Items.get ("Options.MiscOptions") returns a ContentList of all misc options. */
+    AContent get (char[] item) {
+	char[] h = head (item);
+	if (h == "Options") {
+	    if (item is null) {
+		if (Options.allContentList is null) {
+		    AContent[] list;
+		    list.length = Options.optionsClasses.length;
+		    size_t i;
+		    foreach (n,opts; Options.optionsClasses) {
+			if (opts.contentList is null)
+			    loadTransl (opts, n);
+			list[i++] = opts.contentList;
+		    }
+		    Options.allContentList = new ContentList (h, list);
+		    Options.allContentList.name ("Options");	//FIXME
+		}
+		return Options.allContentList;
+	    }
+	    h = head (item);
+	    auto p = h in Options.optionsClasses;
+	    if (p) {
+		if (p.contentList is null)
+		    loadTransl (*p, h);
+		
+		if (item == null)
+		    return p.contentList;
+		
+		auto q = (h = head (item)) in p.content;
+		if (q && item is null)	// enforce item is an exact match
+		    return *q;
+	    }
+	} else if (h == "quit" && item is null) {
+	    quit.name ("Quit");	//FIXME
+	    return quit;
+	}
+	throw new ContentItemException (h);
+    }
+    
+private:
+    /** Takes the string "head.tail" where tail may contain '.' but head does not, returns "head",
+     * with str set to "tail". */
+    char[] head (ref char[] str) {
+	size_t i = 0;
+	while (i < str.length && str[i] != '.')
+	    ++i;
+	char[] ret = str[0..i];
+	if (i == str.length)
+	    str = null;
+	else
+	    str = str[i+1..$];
+	return ret;
+    }
+    
+    void loadTransl (Options p, char[] n) {
+	debug logger.trace ("Loading translation strings for Options."~n);
+	Translation trans = Translation.load ("L10n/"~n);
+	Translation.Entry transled = trans.getStruct (n);
+	p.contentList = new ContentList (n, p.content);
+	p.contentList.name (transled.name, transled.desc);
+	foreach (s, v; p.content) {
+	    transled = trans.getStruct (s);
+	    v.name (transled.name, transled.desc);
+	}
+    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/Services.d	Sat Nov 29 12:36:39 2008 +0000
@@ -0,0 +1,57 @@
+/* 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/>. */
+
+// NOTE: This module is currently unused and untested.
+// FIXME: write unittests
+
+/** Content services.
+ *
+ * Services act on a content, potentially modifying it. They have the form:
+ * ---
+ * bool service (ref Content c);
+ * ---
+ * where c is the content acted on, and the return value is true if the content was changed (useful
+ * to know whether caches need updating or not).
+ */
+module mde.content.Services;
+
+bool copy (ref Content c) {
+    // Put a duplicate on the clipboard, so later changes don't affect what's on the clipboard.
+    clipboard = c.dup;
+    return false;
+}
+
+bool paste (ref ContentText c) {
+    if (clipboard is null)
+        return false;	// no item on clipboard, so don't do anything
+    c = clipboard.toText;
+    return true;
+}
+
+bool paste (ref ContentInt c) {
+    if (clipboard is null)
+        return false;	// no item on clipboard, so don't do anything
+    c = clipboard.toInt;
+    return true;
+}
+
+bool link (ref ContentText c) {
+    if (clipboard is null)
+        return false;	// no item on clipboard, so don't do anything
+    c = clipboard;
+    return true;
+}
+
+Content clipboard;
--- a/mde/gui/WidgetManager.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/WidgetManager.d	Sat Nov 29 12:36:39 2008 +0000
@@ -212,7 +212,7 @@
 
 import mde.gui.exception;
 import mde.gui.widget.Ifaces;
-import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
+import mde.content.Content;
 import mde.gui.widget.createWidget;
 
 import mde.file.mergetag.Reader;
--- a/mde/gui/content/Content.d	Wed Nov 26 13:07:46 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,384 +0,0 @@
-/* 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/>. */
-
-/** The content system − common types.
- */
-module mde.gui.content.Content;
-
-//FIXME: efficient conversions? Need to dup result when formatting a string anyway?
-import Int = tango.text.convert.Integer;
-import Float = tango.text.convert.Float;
-import derelict.sdl.keysym;
-
-debug {
-    import tango.util.log.Log : Log, Logger;
-    private Logger logger;
-    static this () {
-        logger = Log.getLogger ("mde.gui.content.Content");
-    }
-}
-
-/** IContent − interface for all Content classes.
- *
- * Services like copy/paste could work on universal content. However, they would need to run a
- * conversion to the appropriate type (or try next-oldest item on clipboard?).
- *
- * Currently Content instances can only have their data set on creation.
- * Each Content class should provide a method to get it's content, e.g. the method text().
- *
- * Extensions to content:
- * 
- * These extensions require that a content can notify any dependants of changes.
- * 
- * Use as a state switch (one option from an enumeration). E.g. a button/selection box could set a
- * state, and a tabbed box could show a tab based on this. Or could represent an option.
- */
-interface IContent
-{
-    /** Generically return strings.
-     *
-     * This serves two purposes: generically returning a string of/related to the content (i == 0),
-     * and returning associated descriptors. Functions should adhere to (or add to) this table.
-     *
-     *  $(TABLE
-     *  $(TR $(TH i) $(TH returns))
-     *  $(TR $(TD 0) $(TD value))
-     *  $(TR $(TD 1) $(TD Translated name or null))
-     *  $(TR $(TD 2) $(TD Translated description or null))
-     *  $(TR $(TD other) $(TD null))
-     *  ) */
-    char[] toString (uint i);
-}
-
-/** A generic way to handle a list of type IContent. */
-class ContentList : IContent
-{
-    this (IContent[] list = null, char[] n = null, char[] d = null) {
-	list_ = list;
-	name_ = n;
-	desc_ = d;
-    }
-    this (ValueContent[char[]] l, char[] n = null, char[] d = null) {
-	list_.length = l.length;
-	size_t i;
-	foreach (c; l)
-	    list_[i++] = c;
-	name_ = n;
-	desc_ = d;
-    }
-    
-    char[] toString (uint i) {
-	return i == 0 ? Int.toString (list_.length) ~ " elements"
-             : i == 1 ? name_
-             : i == 2 ? desc_
-             : null;
-    }
-    
-    IContent[] list () {
-	return list_;
-    }
-    ContentList list (IContent[] list) {
-	list_ = list;
-	return this;
-    }
-    
-protected:
-    IContent[] list_;
-    char[] name_, desc_;	// name and description
-}
-
-/** Created on errors to display and log a message. */
-class ErrorContent : IContent
-{
-    this (char[] msg) {
-	msg_ = msg;
-    }
-    
-    char[] toString (uint i) {
-	return i == 0 ? msg_
-	     : i == 1 ? "Error"
-	     : null;
-    }
-    
-protected:
-    char[] msg_;
-}
-
-/** Base class for content containing a simple value.
- *
- * All derived classes should have the following functions:
- * ---
- *  this (char[] symbol, T val = /+ default value +/);
- *  BoolContent addChangeCb (void delegate (char[] symbol,T value) cb);	// add a callback called on any change
- *  void assignNoCB (T val);	// assign val, but without calling callbacks (for Options)
- *  void opAssign (T val);	// assign val, calling callbacks
- *  T opCall ();		// return value
- *  alias opCall opCast;
- *  void endEdit ();		// Converts sv and assigns to self, calling callbacks
- * ---
- * On any assignation (by this, assignNoCB, opAssign) the value should be converted to a string and
- * assigned to sv, and pos should be clamped to [0,sv.length] (i.e. enforce pos <= sv.length). */
-abstract class ValueContent : IContent
-{
-    protected this () {}
-    
-    void name (char[] n, char[] d = null) {
-        name_ = n;
-        desc_ = d;
-    }
-    
-    /// Get the text.
-    char[] toString (uint i) {
-        return i == 0 ? sv
-             : i == 1 ? name_
-             : i == 2 ? desc_
-             : null;
-    }
-    
-    /** Acts on a keystroke and returns the new value.
-     *
-     * Supports one-line editing: left/right, home/end, backspace/delete. */
-    char[] keyStroke (ushort sym, char[] i) {
-	debug assert (i.length, "TextContent.keyStroke: no value (??)");	// impossible?
-	char k = *i;
-	if (k > 0x20) {
-	    if (k == 0x7f) {		// delete
-		size_t p = pos;
-		if (p < sv.length) ++p;
-		while (p < sv.length && (sv[p] & 0x80) && !(sv[p] & 0x40))
-		    ++p;
-		sv = sv[0..pos] ~ sv[p..$];
-	    } else {			// insert character
-		char[] tail = sv[pos..$];
-		sv.length = sv.length + i.length;
-		size_t npos = pos+i.length;
-		if (tail) sv[npos..$] = tail.dup;	// cannot assign with overlapping ranges
-		    sv[pos..npos] = i;
-		pos = npos;
-	    }
-	} else {			// use sym; many keys output 0
-	    if (sym == SDLK_BACKSPACE) {	// backspace; k == 0x8
-		char[] tail = sv[pos..$];
-		if (pos) --pos;
-		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
-		    --pos;
-		sv = sv[0..pos] ~ tail;
-	    } else if (sym == SDLK_LEFT) {
-		if (pos) --pos;
-		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
-		    --pos;
-	    } else if (sym == SDLK_RIGHT) {
-		if (pos < sv.length) ++pos;
-		while (pos < sv.length && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
-		    ++pos;
-	    } else if (sym == SDLK_HOME || sym == SDLK_UP) {
-		pos = 0;
-	    } else if (sym == SDLK_END || sym == SDLK_DOWN) {
-		pos = sv.length;
-	    } else
-		debug logger.trace ("Symbol: {}", sym);
-	}
-	return sv;
-    }
-    
-    size_t editIndex () {
-	size_t i = 0;
-	for (size_t p = 0; p < pos; ++p)
-	    if (!(sv[p] & 0x80) || sv[p] & 0x40)
-		++i;
-	return i;
-    }
-    
-    /// Call at the end of an edit to convert sv to v and call callbacks
-    void endEdit ();
-protected:
-    char[] sv;		// string of value; updated on assignment for displaying and editing
-    size_t pos;		// editing position; used by keyStroke
-    char[] symb;
-    char[] name_, desc_;// name and description
-}
-
-template VContentN(T) {
-    static if (is(T == bool)) {
-        const char[] VContentN = "BoolContent";
-    } else static if (is(T == int)) {
-        const char[] VContentN = "IntContent";
-    } else static if (is(T == double)) {
-        const char[] VContentN = "DoubleContent";
-    } else static if (is(T == char[])) {
-        const char[] VContentN = "TextContent";
-    } else
-        static assert (false, "No ValueContent of type "~T.stringof);
-}
-
-class BoolContent : ValueContent
-{
-    /** Create a content with _symbol name symbol. */
-    this (char[] symbol, bool val = false) {
-        symb = symbol;
-	assignNoCB (val);
-    }
-    
-    /** Adds cb to the list of callback functions called when the value is changed. Returns this. */
-    BoolContent addChangeCb (void delegate (char[] symbol,bool value) cb) {
-        cngCb ~= cb;
-        return this;
-    }
-    
-    void assignNoCB (bool val) {
-	v = val;
-	sv = v ? "true" : "false";
-	if (pos > sv.length) pos = sv.length;
-    }
-    void opAssign (bool val) {
-	assignNoCB (val);
-	foreach (cb; cngCb)
-            cb(symb, val);
-    }
-    bool opCall () {
-        return v;
-    }
-    alias opCall opCast;
-    
-    override void endEdit () {
-	v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
-	foreach (cb; cngCb)
-	    cb(symb, v);
-    }
-    
-protected:
-    bool v;
-    void delegate (char[],bool)[] cngCb;       // change callbacks
-}
-
-/** Text content. */
-class TextContent : ValueContent
-{
-    this (char[] symbol, char[] val = null) {
-        symb = symbol;
-	v = val;
-    }
-    
-    /** Adds cb to the list of callback functions called when the value is changed. Returns this. */
-    TextContent addChangeCb (void delegate (char[] symbol,char[] value) cb) {
-        cngCb ~= cb;
-        return this;
-    }
-    
-    void assignNoCB (char[] val) {
-	v = val;
-	if (pos > sv.length) pos = sv.length;
-    }
-    void opAssign (char[] val) {
-	assignNoCB (val);
-        foreach (cb; cngCb)
-            cb(symb, val);
-    }
-    char[] opCall () {
-        return v;
-    }
-    alias opCall opCast;
-    
-    override void endEdit () {
-	foreach (cb; cngCb)
-	    cb(symb, v);
-    }
-    
-protected:
-    alias sv v;		// don't need separate v and sv in this case
-    void delegate (char[],char[])[] cngCb;
-}
-
-/** Integer content. */
-class IntContent : ValueContent
-{
-    /** Create a content with _symbol name symbol. */
-    this (char[] symbol, int val = 0) {
-        symb = symbol;
-	assignNoCB (val);
-    }
-    
-    /** Adds cb to the list of callback functions called when the value is changed. Returns this. */
-    IntContent addChangeCb (void delegate (char[] symbol,int value) cb) {
-        cngCb ~= cb;
-        return this;
-    }
-    
-    void assignNoCB (int val) {
-	v = val;
-	sv = Int.toString (v);
-	if (pos > sv.length) pos = sv.length;
-    }
-    void opAssign (int val) {
-	assignNoCB (val);
-	foreach (cb; cngCb)
-            cb(symb, val);
-    }
-    int opCall () {
-        return v;
-    }
-    alias opCall opCast;
-    
-    override void endEdit () {
-	v = Int.toInt (sv);
-	foreach (cb; cngCb)
-	    cb(symb, v);
-    }
-    
-protected:
-    int v;
-    void delegate (char[],int)[] cngCb;
-}
-
-/** Double content. */
-class DoubleContent : ValueContent
-{
-    /** Create a content with _symbol name symbol. */
-    this (char[] symbol, double val = 0) {
-        symb = symbol;
-	assignNoCB (val);
-    }
-    
-    /** Adds cb to the list of callback functions called when the value is changed. Returns this. */
-    DoubleContent addChangeCb (void delegate (char[] symbol,double value) cb) {
-        cngCb ~= cb;
-        return this;
-    }
-    
-    void assignNoCB (double val) {
-	v = val;
-	sv = Float.toString (v);
-	if (pos > sv.length) pos = sv.length;
-    }
-    void opAssign (double val) {
-	assignNoCB (val);
-	foreach (cb; cngCb)
-            cb(symb, val);
-    }
-    double opCall () {
-        return v;
-    }
-    alias opCall opCast;
-    
-    override void endEdit () {
-	v = Float.toFloat (sv);
-	foreach (cb; cngCb)
-	    cb(symb, v);
-    }
-    
-protected:
-    double v;
-    void delegate (char[],double)[] cngCb;
-}
--- a/mde/gui/content/Items.d	Wed Nov 26 13:07:46 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/* 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/>. */
-
-/**************************************************************************************************
- * A generic way to access content items.
- *************************************************************************************************/
-module mde.gui.content.ContentItems;
-
-import mde.gui.content.Content;
-import mde.gui.exception;
-
-import mde.lookup.Options;
-import mde.lookup.Translation;
-
-debug {
-    import tango.util.log.Log : Log, Logger;
-    private Logger logger;
-    static this () {
-	logger = Log.getLogger ("mde.gui.content.Items");
-    }
-}
-
-    /** Get a specific content item.
-     *
-     * E.g. get ("Options.MiscOptions.L10n") returns miscOpts.L10n,
-     * Items.get ("Options.MiscOptions") returns a ContentList of all misc options. */
-    IContent get (char[] item) {
-	char[] h = head (item);
-	if (h == "Options")
-	    return getOptions (item);
-	throw new ContentItemException (h);
-    }
-    
-    /** Same as calling get("Options."~item). */
-    IContent getOptions (char[] item) {
-	if (item is null) {
-	    IContent[] list;
-	    list.length = Options.optionsClasses.length;
-	    size_t i;
-	    foreach (n,opts; Options.optionsClasses) {
-		if (opts.name is null) loadTransl (opts, n);
-		list[i++] = new ContentList (opts.content, opts.name, opts.desc);
-	    }
-		
-	    return new ContentList (list, "Options");
-	}
-	char[] h = head (item);
-	auto p = h in Options.optionsClasses;
-	if (p) {
-	    if (p.name is null) loadTransl (*p, h);
-	    
-	    if (item == null)
-		return new ContentList (p.content, p.name, p.desc);
-	    
-	    auto q = (h = head (item)) in p.content;
-	    if (q && item is null)	// enforce item is an exact match
-		return *q;
-	}
-	throw new ContentItemException (h);
-    }
-    
-private:
-    /** Takes the string "head.tail" where tail may contain '.' but head does not, returns "head",
-     * with str set to "tail". */
-    char[] head (ref char[] str) {
-	size_t i = 0;
-	while (i < str.length && str[i] != '.')
-	    ++i;
-	char[] ret = str[0..i];
-	if (i == str.length)
-	    str = null;
-	else
-	    str = str[i+1..$];
-	return ret;
-    }
-    
-    void loadTransl (Options p, char[] n) {
-	debug logger.trace ("Loading translation strings for Options."~n);
-	Translation trans = Translation.load ("L10n/"~n);
-	Translation.Entry transled = trans.getStruct (n);
-	p.name = transled.name;
-	p.desc = transled.desc;
-	foreach (s, v; p.content) {
-	    transled = trans.getStruct (s);
-	    v.name (transled.name, transled.desc);
-	}
-    }
--- a/mde/gui/content/Services.d	Wed Nov 26 13:07:46 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/* 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/>. */
-
-// NOTE: This module is currently unused and untested.
-// FIXME: write unittests
-
-/** Content services.
- *
- * Services act on a content, potentially modifying it. They have the form:
- * ---
- * bool service (ref Content c);
- * ---
- * where c is the content acted on, and the return value is true if the content was changed (useful
- * to know whether caches need updating or not).
- */
-module mde.gui.content.Services;
-
-bool copy (ref Content c) {
-    // Put a duplicate on the clipboard, so later changes don't affect what's on the clipboard.
-    clipboard = c.dup;
-    return false;
-}
-
-bool paste (ref ContentText c) {
-    if (clipboard is null)
-        return false;	// no item on clipboard, so don't do anything
-    c = clipboard.toText;
-    return true;
-}
-
-bool paste (ref ContentInt c) {
-    if (clipboard is null)
-        return false;	// no item on clipboard, so don't do anything
-    c = clipboard.toInt;
-    return true;
-}
-
-bool link (ref ContentText c) {
-    if (clipboard is null)
-        return false;	// no item on clipboard, so don't do anything
-    c = clipboard;
-    return true;
-}
-
-Content clipboard;
--- a/mde/gui/widget/Ifaces.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/Ifaces.d	Sat Nov 29 12:36:39 2008 +0000
@@ -25,7 +25,7 @@
 
 public import mde.gui.types;
 public import mde.gui.renderer.IRenderer;
-import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
+import mde.content.Content;
 
 
 /*************************************************************************************************
--- a/mde/gui/widget/TextWidget.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/TextWidget.d	Sat Nov 29 12:36:39 2008 +0000
@@ -22,7 +22,7 @@
 import mde.gui.widget.Widget;
 import mde.gui.exception;
 import mde.gui.renderer.IRenderer;
-import mde.gui.content.Content;
+import mde.content.Content;
 
 import tango.util.log.Log : Log, Logger;
 private Logger logger;
@@ -86,29 +86,3 @@
     IContent content;
     int index;
 }
-
-class TextButtonWidget : AButtonWidget
-{
-    this (IWidgetManager mgr, widgetID id, WidgetData data) {
-	WDCheck (data, 1, 1);
-	adapter = mgr.renderer.getAdapter (data.strings[0]);
-	adapter.getDimensions (mw, mh);
-	w = mw;
-	h = mh;
-	super (mgr, id, data);
-    }
-    
-    void draw () {
-	super.draw();
-	adapter.draw (x,y);
-    }
-    
-    void activated () {
-	logger.info ("Button \""~adapter.text~"\" pressed");
-    }
-    
-protected:
-    IRenderer.TextAdapter adapter;
-    IContent content;
-    int index;
-}
\ No newline at end of file
--- a/mde/gui/widget/createWidget.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/createWidget.d	Sat Nov 29 12:36:39 2008 +0000
@@ -21,7 +21,7 @@
 
 import mde.gui.widget.Ifaces;
 import mde.gui.exception : WidgetDataException;
-import mde.gui.content.Content; //NOTE - maybe move IContent to a separate module
+import mde.content.Content;
 
 // Widgets to create:
 import mde.gui.widget.layout;
@@ -97,7 +97,6 @@
     
     // buttons: 0x10
     Button		= 0x10,
-    TextButton		= 0x11,
     
     // labels: 0x20
     ContentLabel	= TAKES_CONTENT | 0x20,
@@ -110,7 +109,8 @@
     // content widgets: 0x40
     DisplayContent	= TAKES_CONTENT | 0x40,
     BoolContent		= TAKES_CONTENT | 0x41,
-    ValueContent	= TAKES_CONTENT | 0x42,
+    AStringContent	= TAKES_CONTENT | 0x42,
+    ButtonContent	= TAKES_CONTENT | 0x43,
     
     GridLayout		= TAKES_CONTENT | PARENT | 0x100,
     ContentList		= TAKES_CONTENT | PARENT | 0x110,
@@ -126,13 +126,13 @@
         "SizableBlank",
         "Debug",
         "Button",
-	"TextButton",
 	"TextLabel",
 	"addContent",
 	"ContentLabel",
         "DisplayContent",
         "BoolContent",
-	"ValueContent",
+	"AStringContent",
+	"ButtonContent",
         "editContent",
         "FloatingArea",
         "GridLayout",
--- a/mde/gui/widget/layout.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/layout.d	Sat Nov 29 12:36:39 2008 +0000
@@ -19,7 +19,7 @@
 import mde.gui.widget.Widget;
 import mde.gui.exception;
 
-import mde.gui.content.Content;
+import mde.content.Content;
 
 import tango.util.container.HashMap;
 
--- a/mde/gui/widget/miscContent.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/miscContent.d	Sat Nov 29 12:36:39 2008 +0000
@@ -24,8 +24,8 @@
 import mde.gui.widget.layout;
 
 import mde.gui.renderer.IRenderer;
-import mde.gui.content.Content;
-import Items = mde.gui.content.Items;
+import mde.content.AStringContent;
+import Items = mde.content.Items;
 
 /*************************************************************************************************
  * A function which uses Items.get (data.strings[0]) to get a content and creates a widget from
@@ -52,8 +52,8 @@
     if (c is null) throw new ContentException;
     if (cast(BoolContent) c)
         return new BoolContentWidget(mgr,id,data,c);
-    else if (cast(ValueContent) c)
-	return new ValueContentWidget(mgr,id,data,c);
+    else if (cast(AStringContent) c)
+	return new AStringContentWidget(mgr,id,data,c);
     else if (cast(ContentList) c)
 	return new ContentListWidget(mgr,id,data,c);
     else        // generic uneditable option
@@ -85,3 +85,32 @@
     BoolContent content;
 }
 
+/// A button connected to an EventContent
+class ButtonContentWidget : AButtonWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
+	WDCheck (data, 1);
+	content = cast(EventContent) c;
+	if (!content) throw new ContentException ();
+	adapter = mgr.renderer.getAdapter (content.toString (1));
+	adapter.getDimensions (mw, mh);
+	w = mw;
+	h = mh;
+	super (mgr, id, data);
+    }
+    
+    void draw () {
+	super.draw();
+	adapter.draw (x,y);
+    }
+    
+    void activated () {
+	content.endEvent;
+    }
+    
+    protected:
+	IRenderer.TextAdapter adapter;
+	EventContent content;
+	int index;
+}
+
--- a/mde/gui/widget/textContent.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/gui/widget/textContent.d	Sat Nov 29 12:36:39 2008 +0000
@@ -21,7 +21,7 @@
 import mde.gui.exception;
 import mde.gui.renderer.IRenderer;
 
-import mde.gui.content.Content;
+import mde.content.AStringContent;
 
 debug {
     import tango.util.log.Log : Log, Logger;
@@ -42,16 +42,16 @@
 	super (mgr, id, data);
     }
     
-    protected:
-	IContent content;
+protected:
+    IContent content;
 }
 
 /// Capable of editing any ValueContent class
-class ValueContentWidget : ATextWidget
+class AStringContentWidget : ATextWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
         WDMinCheck(data, 1);
-        content = cast(ValueContent) c;
+        content = cast(AStringContent) c;
         if (!content) //content = new TextContent (null, null);
 	    throw new ContentException ();
         adapter = mgr.renderer.getAdapter (content.toString(0));
@@ -78,5 +78,5 @@
     }
     
 protected:
-    ValueContent content;
+    AStringContent content;
 }
--- a/mde/imde.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/imde.d	Sat Nov 29 12:36:39 2008 +0000
@@ -19,13 +19,19 @@
 
 import mde.input.Input;
 import mde.scheduler.Scheduler;
+import mde.content.Content;
 
 static this () {
     // Make these available to all importing modules' static CTORs, as well as during init.
     input = new Input();
     mainSchedule = new Scheduler;
+    
+    quit = (new EventContent("quit")).addCallback ((AContent){
+	run = false;
+    });
 }
 
+EventContent quit;	/// A content triggering mde to halt
 
 Scheduler mainSchedule; /// The schedule used by the main loop.
 
--- a/mde/lookup/Options.d	Wed Nov 26 13:07:46 2008 +0000
+++ b/mde/lookup/Options.d	Sat Nov 29 12:36:39 2008 +0000
@@ -20,7 +20,7 @@
 import mde.setup.paths;
 import mde.exception;
 
-public import mde.gui.content.Content;
+public import mde.content.AStringContent;
 
 import mde.file.mergetag.Reader;
 import mde.file.mergetag.Writer;
@@ -96,8 +96,8 @@
             const char[] ifBlock = `if (tp == "`~T.stringof~`") {
     auto p = id in opts;
     if (p) {
-        auto q = cast(`~VContentN!(T)~`) (*p);
-        if (q) q.assignNoCB = parseTo!(`~T.stringof~`) (dt);
+        auto q = cast(`~ContentN!(T)~`) (*p);
+        if (q) q.assignNoCb = parseTo!(`~T.stringof~`) (dt);
     }
 }`;
             static if (A.length)
@@ -120,6 +120,8 @@
         private Options[ID] subClasses;
         private bool changed = false;	// any changes at all, i.e. do we need to save?
 	
+	ContentList allContentList;	/// Initially null; created by mde.content.Items on use.
+	
     	/* Load/save options from file.
          *
          * If the file doesn't exist, no reading is attempted (options are left at default values).
@@ -183,7 +185,7 @@
     
     //BEGIN Non-static
     /// Get all Options stored with a ValueContent.
-    ValueContent[char[]] content() {
+    AContent[char[]] content() {
         return opts;
     }
     
@@ -192,13 +194,13 @@
      * This can be overridden to enforce limits on option variables, etc. */
     protected void validate() {}
     
-    /** Translated name and description of the instance. mde.gui.content.Items loads these and the
-     * translation strings of all enclosed options simultaneously. */
-    char[] name, desc;
+    /** All content in a ContentList. Initially null; mde.content.Items creates this and loads the
+    * translation strings of all sub-content upon first request involving this Options instance. */
+    ContentList contentList;
     
     protected {
         OptionChanges optionChanges;	// all changes to options (for saving)
-        ValueContent[char[]] opts;	// generic list of option values
+	AContent[char[]] opts;	// generic list of option values
     }
     
     //BEGIN Mergetag loading/saving code
@@ -267,7 +269,7 @@
             else static if (A[0] == ' ')
                 const char[] createContents = createContents!(T,A[1..$]);
             else
-                const char[] createContents = "opts[\""~A[0..cIndex!(A)]~"\"] = " ~ A[0..cIndex!(A)]~ " = (new "~VContentN!(T)~" (\""~A[0..cIndex!(A)]~"\")).addChangeCb (&optionChanges.set);\n"~
+                const char[] createContents = "opts[\""~A[0..cIndex!(A)]~"\"] = (" ~ A[0..cIndex!(A)]~ " = new "~ContentN!(T)~" (\""~A[0..cIndex!(A)]~"\")).addCallback (&optionChanges.set);\n"~
                 createContents!(T,A[cIndex!(A)+1..$]);
         }
         // for recursing on TYPES
@@ -280,7 +282,7 @@
         }
         template declValsInternal(char[] A, B...) {
             static if (B.length) {
-                const char[] declValsInternal = catOrNothing!(VContentN!(B[0]),parseT!(B[0].stringof,A)) ~ declValsInternal!(A,B[1..$]);
+                const char[] declValsInternal = catOrNothing!(ContentN!(B[0]),parseT!(B[0].stringof,A)) ~ declValsInternal!(A,B[1..$]);
             } else
                 const char[] declValsInternal = ``;
         }
@@ -353,17 +355,6 @@
             } else
                 const char[] Vars = ``;
         }
-        // For set
-        template Set(A...) {
-            static if (A.length) {
-                const char[] Set= `void set (char[] s,`~A[0].stringof~` v) {
-   Options.changed = true;
-   `~TName!(A[0])~`s[s] = v;
-}`~ Set!(A[1..$]);
-            } else
-                const char[] Set = ``;
-        }
-        
         // For addTag
         template addTagMixin(T, A...) {
             const char[] ifBlock = `if (tp == "`~T.stringof~`") {
@@ -390,9 +381,26 @@
     // 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));
-    // set (char[] symbol, T value);    (not templates but for each type T)
-    mixin(Set!(TYPES));
     
+    void set (AContent c) {
+	union U {
+	    BoolContent bc;
+	    StringContent sc;
+	    IntContent ic;
+	    DoubleContent dc;
+	}
+	U u;
+	if ((u.bc = cast(BoolContent) c) !is null)
+	    bools[u.bc.symbol] = u.bc();
+	else if ((u.sc = cast(StringContent) c) !is null)
+	    charAs[u.sc.symbol] = u.sc();
+	else if ((u.ic = cast(IntContent) c) !is null)
+	    ints[u.ic.symbol] = u.ic();
+	else if ((u.dc = cast(DoubleContent) c) !is null)
+	    doubles[u.dc.symbol] = u.dc();
+	Options.changed = true;
+    }
+	
     this () {}
     
     //BEGIN Mergetag loading/saving code