changeset 112:fe061009029d

EnumContent; log level can be selected from a popup list. New EnumContent, with code to load translations in Items. Editable as an AStringContent. Hacked OptionsMisc to use an EnumContent. Implemented a EnumContentWidget providing a pop-up list to select from (still needs improving). Moved IContent to its own module. ContentExceptions thrown via WDCCheck now. Fixed a small bug with reloading translations.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 13 Dec 2008 12:54:43 +0000
parents 1655693702fc
children 9824bee909fd
files codeDoc/jobs.txt data/L10n/en-GB.mtt mde/content/AStringContent.d mde/content/Content.d mde/content/IContent.d mde/content/Items.d mde/gui/WidgetManager.d mde/gui/exception.d mde/gui/widget/Ifaces.d mde/gui/widget/Popup.d mde/gui/widget/TextWidget.d mde/gui/widget/Widget.d mde/gui/widget/createWidget.d mde/gui/widget/layout.d mde/gui/widget/miscContent.d mde/gui/widget/textContent.d mde/lookup/Options.d
diffstat 17 files changed, 328 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Sat Dec 06 17:41:42 2008 +0000
+++ b/codeDoc/jobs.txt	Sat Dec 13 12:54:43 2008 +0000
@@ -13,6 +13,7 @@
 3   Use of dtors - don't rely on them? Or what happens when init throws during creation - relying on undefined behaviour.
 3   glBindTexture not working with non-0 index - perhaps use a higher level graphics library at some point.
 3   Windows building/compatibility (currently partial) - tango/sys/win32/SpecialPath.d
+2   Possibility: Could have a content method "update (T*)" to add a synchronized variable. Or could store both a content and a syncronized variable for each option within Options.
 2   Add callback to miscOpts.L10n reverting the option to it's last value if no file is found and reloading translations.
 2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering. Also remove dataset and force use of section creator callback?
 2   Options need a "level": simple options, for advanced users, for debugging only, etc.
--- a/data/L10n/en-GB.mtt	Sat Dec 06 17:41:42 2008 +0000
+++ b/data/L10n/en-GB.mtt	Sat Dec 13 12:54:43 2008 +0000
@@ -11,7 +11,13 @@
 {MiscOptions}
 <entry|MiscOptions={0:"Miscellaneous options"}>
 <entry|maxThreads={0:"Max threads",1:"Maximum number of threads to use in mde (currently only applies to init stages run in parallel)."}>
-<entry|logLevel={0:"Logging level",1:"Lowest level to log messages; 0=trace, 1=info (default), 2=warn, 3=error, 4=fatal, 5=none."}>
+<entry|logLevel={0:"Logging level",1:"Log messages of this level and higher."}>
+<entry|logLevel.Trace={0:"Trace"}>
+<entry|logLevel.Info={0:"Info"}>
+<entry|logLevel.Warn={0:"Warn"}>
+<entry|logLevel.Error={0:"Error"}>
+<entry|logLevel.Fatal={0:"Fatal"}>
+<entry|logLevel.None={0:"None"}>
 <entry|logOutput={0:"Logging output",1:"Output to: 0=nowhere, 1=the console, 2=a file, 3=both"}>
 <entry|L10n={0:"Localisation",1:"Specifies the language to use."}>
 <entry|pollInterval={0:"Polling interval",1:"Delay in main loop to limit CPU usage"}>
--- a/mde/content/AStringContent.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/content/AStringContent.d	Sat Dec 13 12:54:43 2008 +0000
@@ -50,7 +50,7 @@
  *
  * All derived classes should have the following functions:
  * ---
- *  void endEdit ();	// Should convert sv and assign to self, then call endEvent
+ *  char[] 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
@@ -126,8 +126,8 @@
 	return i;
     }
     
-    /// Call after editing a string
-    void endEdit ();
+    /** Call after editing a string; return new string (may be changed/reverted). */
+    char[] endEdit ();
     
 protected:
     char[] sv;		// string of value; updated on assignment for displaying and editing
@@ -156,9 +156,10 @@
     }
     alias opCall opCast;
     
-    override void endEdit () {
+    override char[] endEdit () {
 	v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
 	endEvent;
+	return sv;
     }
     
 protected:
@@ -186,8 +187,9 @@
     }
     alias opCall opCast;
     
-    override void endEdit () {
+    override char[] endEdit () {
 	endEvent;
+	return sv;
     }
     
 protected:
@@ -217,9 +219,10 @@
     }
     alias opCall opCast;
     
-    override void endEdit () {
+    override char[] endEdit () {
 	v = Int.toInt (sv);
 	endEvent;
+	return sv;
     }
     
 protected:
@@ -249,11 +252,77 @@
     }
     alias opCall opCast;
     
-    override void endEdit () {
+    override char[] endEdit () {
 	v = Float.toFloat (sv);
 	endEvent;
+	return sv;
     }
     
 protected:
     double v;
 }
+
+/** A content representing an enumeration. */
+class EnumContent : IntContent
+{
+    /** CTOR.
+    *
+    * Params:
+    *	enumSymbols = Symbol names for each
+    *	val = which value is active; must be in [0,length-1]
+    */
+    this (char[] symbol, char[][] enumSymbols, int val = 0) {
+	this.enumSymbols = enumSymbols;
+	enumName = enumSymbols.dup;	// dup top array but not strings; elements of top array are replaced when tranlated
+	enumDesc.length = enumSymbols.length;	// top array should always be allocated
+	super (symbol, val);
+    }
+    
+    void nameEnum (uint i, char[] name, char[] desc = null) {
+	enumName[i] = name;
+	enumDesc[i] = desc;
+	if (v == i)
+	    sv = name;	// keep sv in sync
+    }
+    
+    char[] toString (uint i) {
+	return i >= 3 ? enumName[i-3]
+	: i == 0 ? enumName[v]
+	: i == 1 ? name_
+	: i == 2 ? desc_
+	: null;
+    }
+    
+    void assignNoCb (int val) {
+	if (val >= enumSymbols.length || val < 0) {
+	    logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~enumName[v]);
+	    return;
+	}
+	v = val;
+	sv = enumName[v];
+	if (pos > sv.length) pos = sv.length;
+    }
+    
+    override char[] endEdit () {
+	foreach (i,n; enumName)
+	    if (sv == n) {
+		v = i;
+		goto break1;
+	    }
+	
+	logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~enumName[v]);
+	sv = enumName[v];	// sv was edited; revert
+	if (pos > sv.length) pos = sv.length;
+	
+	break1:
+	endEvent;
+	logger.trace ("All logging levels: {}", enumName);
+	return sv;
+    }
+    
+    final char[][] enumSymbols;
+protected:
+    char[][] enumName;
+    char[][] enumDesc;
+    uint value;
+}
--- a/mde/content/Content.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/content/Content.d	Sat Dec 13 12:54:43 2008 +0000
@@ -13,10 +13,12 @@
 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.
- */
+/*************************************************************************************************
+ * The content system − a few content classes.
+ *************************************************************************************************/
 module mde.content.Content;
 
+public import mde.content.IContent;
 import util = mde.util;
 
 debug {
@@ -27,27 +29,6 @@
     }
 }
 
-/** 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.
@@ -63,11 +44,12 @@
 {
     this (char[] symbol) {
 	this.symbol = symbol;
+	name_ = symbol;		// provide a temporary name
     }
     
-    void name (char[] n, char[] d = null) {
-	name_ = n;
-	desc_ = d;
+    void name (char[] name, char[] desc = null) {
+	name_ = name;
+	desc_ = desc;
     }
     
     /** Add a callback. Callbacks are called in the order added. */
@@ -129,17 +111,17 @@
 class ErrorContent : IContent
 {
     this (char[] msg) {
-	msg_ = msg;
+	this.msg = msg;
     }
     
     char[] toString (uint i) {
-	return i == 0 ? msg_
+	return i == 0 ? msg
 	     : i == 1 ? "Error"
 	     : null;
     }
     
 protected:
-    char[] msg_;
+    char[] msg;
 }
 
 /** A Content with no value but able to pass on an event.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/content/IContent.d	Sat Dec 13 12:54:43 2008 +0000
@@ -0,0 +1,40 @@
+/* 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.
+ *************************************************************************************************/
+module mde.content.IContent;
+
+/** 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);
+}
--- a/mde/content/Items.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/content/Items.d	Sat Dec 13 12:54:43 2008 +0000
@@ -97,6 +97,14 @@
 	    foreach (s, v; opts.content) {
 		trle = trl.getStruct (s);
 		v.name (trle.name, trle.desc);
+		EnumContent ec = cast(EnumContent) v;
+		if (ec) {
+		    char[] tp = s ~ ".";
+		    foreach (i,sym; ec.enumSymbols) {
+			trle = trl.getStruct (tp~sym);
+			ec.nameEnum (i, trle.name, trle.desc);
+		    }
+		}
 	    }
 	}
 	
--- a/mde/gui/WidgetManager.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/WidgetManager.d	Sat Dec 13 12:54:43 2008 +0000
@@ -191,6 +191,14 @@
         }
         popups.prepend (popup);
     }
+    void removePopup (IChildWidget widg) {	//FIXME: not optimal (maybe change popups though?)
+	auto i = popups.iterator;
+	foreach (popup; i) {
+	    if (popup.widget is widg)
+		i.remove;
+	}
+	requestRedraw;
+    }
 
     void requestRedraw () {
         imde.mainSchedule.request(imde.SCHEDULE.DRAW);
@@ -247,8 +255,6 @@
     }
     IRenderer rend;
     CircularList!(ActivePopup) popups;// Pop-up [menus] to draw. First element is top popup.
-    wdim w,h;       // area available to the widgets
-    wdim mw,mh;     // minimal area required by widgets (ideally for limiting w,h)
     // callbacks indexed by their frame pointers:
     bool delegate(wdabs cx, wdabs cy, ubyte b, bool state) [void*] clickCallbacks;
     void delegate(wdabs cx, wdabs cy) [void*] motionCallbacks;
@@ -257,8 +263,7 @@
 
 
 import mde.gui.exception;
-import mde.gui.widget.Ifaces;
-import mde.content.Content;
+import mde.content.Content;	// AContent passed to a callback
 import mde.gui.widget.createWidget;
 
 import mde.file.mergetag.Reader;
@@ -446,9 +451,11 @@
     }
     
     /** Called when translation strings have been reloaded. */
-    void reloadStrings (AContent c) {
+    void reloadStrings (AContent) {
 	Items.loadTranslation;
 	child.setup (++setupN, 2);
+	child.setWidth  (w, -1);
+	child.setHeight (h, -1);
 	child.setPosition (0,0);
 	requestRedraw;
     }
@@ -509,6 +516,8 @@
     scope mt.DataSet changesDS;		// changes and sections from user file (used for saving)
     bool loadUserFile = true;		// still need to load user file for saving?
     
+    wdim w,h;				// area available to the widgets
+    wdim mw,mh;				// minimal area required by widgets (ideally for limiting w,h)
     scope IChildWidget child;		// The primary widget.
     uint setupN;			// n to pass to IChildWidget.setup
     
--- a/mde/gui/exception.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/exception.d	Sat Dec 13 12:54:43 2008 +0000
@@ -48,6 +48,9 @@
     this () {
         super ("Unexpected content type");
     }
+    this (Object o) {	// Default, by Widget class's this / WDCCheck
+	super ("Unexpected content type for "~o.classinfo.name);
+    }
     this (char[] msg) {
         super (msg);
     }
--- a/mde/gui/widget/Ifaces.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/Ifaces.d	Sat Dec 13 12:54:43 2008 +0000
@@ -25,7 +25,7 @@
 
 public import mde.gui.types;
 public import mde.gui.renderer.IRenderer;
-import mde.content.Content;
+import mde.content.IContent;
 
 
 /*************************************************************************************************
@@ -91,6 +91,7 @@
     
     /** Add/remove a pop-up [menu] to be drawn. */
     void addPopup (wdabs x, wdabs y, IChildWidget popup);
+    void removePopup (IChildWidget popup);	/// ditto
     
     // User input:
     /** Add a mouse click callback.
--- a/mde/gui/widget/Popup.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/Popup.d	Sat Dec 13 12:54:43 2008 +0000
@@ -17,7 +17,7 @@
 module mde.gui.widget.Popup;
 
 import mde.gui.widget.Widget;
-import mde.content.Content;
+import mde.content.IContent;
 
 /** Shows a "pop-up" widget tree when clicked. */
 class PopupButtonWidget : AButtonWidget
--- a/mde/gui/widget/TextWidget.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/TextWidget.d	Sat Dec 13 12:54:43 2008 +0000
@@ -85,9 +85,8 @@
 class ContentLabelWidget : ATextWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
-        WDCheck (data, 3, 0);
-        content = c;
-        if (!content) throw new ContentException ();
+	content = c;
+	WDCCheck (data, 3, 0, content);
         index = data.ints[1];
         adapter = mgr.renderer.getAdapter (data.ints[2]);
         super (mgr, id,data);
--- a/mde/gui/widget/Widget.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/Widget.d	Sat Dec 13 12:54:43 2008 +0000
@@ -26,6 +26,7 @@
 module mde.gui.widget.Widget;
 
 public import mde.gui.widget.Ifaces;
+import mde.content.IContent;
 import mde.gui.exception;
 
 debug {
@@ -130,9 +131,14 @@
     
 protected:
     /**********************************************************************************************
-     * Widgets may use WDCheck as a utility to check what data holds. Its use is encouraged, so
-     * that the checks can easily be updated should WidgetData be changed. WDMinCheck is similar,
-     * but allows more data than required; it is used by some generic content widgets.
+     * Widgets may use W*Check as a utility to check for existance of data. Its use is encouraged,
+     * so that the checks can easily be updated should WidgetData be changed.
+     * 
+     * Variants:
+     *	WDCheck checks the exact length of integer and string data.
+     *	WDCCheck checks data as WDCheck and that the content passed is valid.
+     *	WDCMinCheck does the same as WDCCheck, but allows more data than required (used by some
+     *	generic widgets).
      * 
      * Params:
      *  data    = the WidgetData to check lengths of
@@ -145,10 +151,20 @@
 	    throw new WidgetDataException (this);
     }
     /** ditto */
-    void WDMinCheck (WidgetData data, size_t n_ints, size_t n_strings = 0) {
+    void WDCCheck (WidgetData data, size_t n_ints, size_t n_strings, IContent c) {
+	if (data.ints.length    != n_ints ||
+	    data.strings.length != n_strings)
+	    throw new WidgetDataException (this);
+	if (c is null)
+	    throw new ContentException (this);
+    }
+    /** ditto */
+    void WDCMinCheck (WidgetData data, size_t n_ints, size_t n_strings, IContent c) {
 	if (data.ints.length    < n_ints ||
 	    data.strings.length < n_strings)
 	    throw new WidgetDataException (this);
+	if (c is null)
+	    throw new ContentException (this);
     }
     
     widgetID id;                // The widget's ID, used for saving data
--- a/mde/gui/widget/createWidget.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/createWidget.d	Sat Dec 13 12:54:43 2008 +0000
@@ -21,7 +21,7 @@
 
 import mde.gui.widget.Ifaces;
 import mde.gui.exception : WidgetDataException;
-import mde.content.Content;
+import mde.content.IContent;
 
 // Widgets to create:
 import mde.gui.widget.layout;
@@ -113,6 +113,7 @@
     BoolContent		= TAKES_CONTENT | 0x41,
     AStringContent	= TAKES_CONTENT | 0x42,
     ButtonContent	= TAKES_CONTENT | 0x43,
+    EnumContent	= TAKES_CONTENT | 0x44,
     
     GridLayout		= TAKES_CONTENT | PARENT | 0x100,
     ContentList		= TAKES_CONTENT | PARENT | 0x110,
@@ -135,6 +136,7 @@
         "BoolContent",
 	"AStringContent",
 	"ButtonContent",
+	"EnumContent",
         "editContent",
         "FloatingArea",
 	"PopupButton",
--- a/mde/gui/widget/layout.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/layout.d	Sat Dec 13 12:54:43 2008 +0000
@@ -101,11 +101,8 @@
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
         debug scope (failure)
                 logger.warn ("TrialContentLayoutWidget: failure");
-        WDCheck (data, 2, 1);
-	
 	cList = cast(ContentList) content;
-	if (cList is null)
-	    throw new ContentException;
+	WDCCheck (data, 2, 1, cList);
 	
         cols = 1;
         if ((rows = cList.list.length) > 0) {
@@ -436,7 +433,6 @@
     /** Like IChildWidget's setup; calls sADD delegates. */
     void setup (uint n, uint flags) {
 	if (n != setup_n) {
-	    logger.trace ("AlignColumns.setup ({}): {}", n, cast(void*)this);
 	    setup_n = n;
 	    setupWidths = false;
 	    reset (minWidth.length);
@@ -462,7 +458,6 @@
      */
     void setWidths (wdim[] data = null) {
 	if (!setupWidths) {
-	    logger.trace ("setWidths");
 	    setupWidths = true;
             if (data || width) {	// use existing/external data: need to check validity
                 if (data) {
--- a/mde/gui/widget/miscContent.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/miscContent.d	Sat Dec 13 12:54:43 2008 +0000
@@ -27,6 +27,14 @@
 import mde.content.AStringContent;
 import Items = mde.content.Items;
 
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+    static this () {
+	logger = Log.getLogger ("mde.gui.widget.miscContent");
+    }
+}
+
 /*************************************************************************************************
  * A function which uses Items.get (data.strings[0]) to get a content and creates a widget from
  * data.ints[1]. The first item in each ints and strings is removed before passing data to the new
@@ -52,9 +60,12 @@
     if (c is null) throw new ContentException;
     if (cast(BoolContent) c)
         return new BoolContentWidget(mgr,id,data,c);
-    else if (cast(AStringContent) c)
-	return new AStringContentWidget(mgr,id,data,c);
-    else if (cast(ContentList) c)
+    else if (cast(AStringContent) c) {
+	if (cast(EnumContent) c)
+	    return new EnumContentWidget(mgr,id,data,c);
+	else
+	    return new AStringContentWidget(mgr,id,data,c);
+    } else if (cast(ContentList) c)
 	return new ContentListWidget(mgr,id,data,c);
     else        // generic uneditable option
         return new DisplayContentWidget(mgr,id,data,c);
@@ -64,9 +75,8 @@
 class BoolContentWidget : AButtonWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
-        WDCheck(data, 1);
-        content = cast(BoolContent) c;
-        if (!content) throw new ContentException ();
+	content = cast(BoolContent) c;
+	WDCCheck(data, 1,0, content);
         wdimPair s = mgr.renderer.getToggleSize;
         w = mw = s.x;
         h = mh = s.y;
@@ -89,9 +99,8 @@
 class ButtonContentWidget : AButtonWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
-	WDCheck (data, 1);
 	content = cast(EventContent) c;
-	if (!content) throw new ContentException ();
+	WDCCheck (data, 1,0, content);
 	adapter = mgr.renderer.getAdapter ();
 	super (mgr, id, data);
     }
@@ -121,4 +130,101 @@
 	EventContent content;
 	int index;
 }
-
+/// Pops up a list of selectable values
+class EnumContentWidget : AButtonWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
+	content = cast(EnumContent) c;
+	WDCCheck (data, 1,0, content);
+	adapter = mgr.renderer.getAdapter;
+	adapter.text = content.toString (0);
+	adapter.getDimensions (mw, mh);
+	w = mw;
+	h = mh;
+	internalWidg = new EnumContentPopup (mgr, id, data, content);
+	super (mgr, id, data);
+    }
+    
+    void changeContent () {
+	adapter.text = content.toString (0);
+	adapter.getDimensions (mw, mh);
+	w = mw;
+	h = mh;
+    }
+    
+    void activated () {
+	mgr.addPopup (x,y, internalWidg);
+    }
+    
+    void draw () {
+	super.draw;
+	adapter.draw (x,y);
+    }
+    
+protected:
+    EnumContentPopup internalWidg;	// setup & saveChanges calls not propegated
+    EnumContent content;
+    IRenderer.TextAdapter adapter;
+    
+    /// The widget which gets popped up
+    class EnumContentPopup : AButtonWidget
+    {
+	/// Assumes EnumContent c isn't null
+	this (IWidgetManager mgr, widgetID id, WidgetData data, EnumContent c) {
+	    content = c;
+	    adapters.length = content.enumSymbols.length;
+	    yPos.length = adapters.length;
+	    // mh is 0
+	    foreach (i, ref adapter; adapters) {
+		adapter = mgr.renderer.getAdapter;
+		adapter.text = content.toString(i+3);
+		wdim aw, ah;
+		adapter.getDimensions (aw,ah);
+		if (mw < aw) mw = aw;
+		yPos[i] = mh;
+		mh += ah;
+	    }
+	    w = mw;
+	    h = mh;
+	    super (mgr, id, data);
+	}
+	
+	void draw () {
+	    super.draw;
+	    foreach (i,yp; yPos) {
+		adapters[i].draw (x,y+yp);
+	    }
+	}
+	
+	/// Called when a mouse click event occurs while held; handles up-click
+	bool clickWhilePushed (wdabs cx, wdabs cy, ubyte b, bool state) {
+	    if (b == 1 && state == false) {
+		cy -= y;
+		if (cx >= x && cx < x+w && cy >= 0 && cy < h) {
+		    uint i = yPos.length-1;
+		    for (; i > 0; --i)
+			if (cy >= yPos[i])
+			    break;
+		    logger.trace ("Setting value: {}", i);
+		    content = i;
+		    mgr.removePopup (this);
+		    changeContent;
+		}
+		
+		pushed = false;
+		mgr.requestRedraw;
+		mgr.removeCallbacks (cast(void*) this);
+		
+		return true;
+	    }
+	    return false;
+	}
+	
+	void activated () {}
+	
+    protected:
+	EnumContent content;
+	IRenderer.TextAdapter[] adapters;	// NOTE: replace with multi-line adapter
+	wdrel[] yPos;	// y position of each adapter
+    }
+}
--- a/mde/gui/widget/textContent.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/gui/widget/textContent.d	Sat Dec 13 12:54:43 2008 +0000
@@ -31,13 +31,12 @@
     }
 }
 
-/// Just displays the content
+/// Just displays the content. Generic − any IContent.
 class DisplayContentWidget : ATextWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
-	WDMinCheck(data, 1);
 	content = c;
-	if (!content) throw new ContentException ();
+	WDCMinCheck(data, 1,0, content);
 	adapter = mgr.renderer.getAdapter ();
 	adapter.text = content.toString(0);
 	super (mgr, id, data);
@@ -47,14 +46,12 @@
     IContent content;
 }
 
-/// Capable of editing any ValueContent class
+/// Text-box for editing a content. Generic − any AStringContent.
 class AStringContentWidget : ATextWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
-        WDMinCheck(data, 1);
-        content = cast(AStringContent) c;
-        if (!content) //content = new TextContent (null, null);
-	    throw new ContentException ();
+	content = cast(AStringContent) c;
+	WDCMinCheck(data, 1,0, content);
         adapter = mgr.renderer.getAdapter ();
 	adapter.text = content.toString(0);
 	super (mgr, id, data);
@@ -77,8 +74,8 @@
 	mgr.requestRedraw;
     }
     void keyFocusLost () {
+	adapter.text = content.endEdit;	// update other users of content relying on callbacks
 	adapter.index;
-	content.endEdit;	// update other users of content relying on callbacks
 	mgr.requestRedraw;
     }
     
--- a/mde/lookup/Options.d	Sat Dec 06 17:41:42 2008 +0000
+++ b/mde/lookup/Options.d	Sat Dec 13 12:54:43 2008 +0000
@@ -430,7 +430,23 @@
     impl creates a this() function; if you want to include your own CTOR see impl's ddoc. */
     const A = "bool exitImmediately; int maxThreads, logLevel, logOutput; double pollInterval; char[] L10n;";
     //pragma (msg, impl!(A));
-    mixin (impl!(A));
+    //mixin (impl!(A));
+    BoolContent exitImmediately;
+    IntContent maxThreads, logOutput;
+    EnumContent logLevel;
+    DoubleContent pollInterval;
+    StringContent L10n;
+    
+    this(char[] name){
+	optionChanges = new OptionChanges;
+	super (name);
+	opts["exitImmediately"] = (exitImmediately = new BoolContent ("exitImmediately")).addCallback (&optionChanges.set);
+	opts["maxThreads"] = (maxThreads = new IntContent ("maxThreads")).addCallback (&optionChanges.set);
+	opts["logLevel"] = (logLevel = new EnumContent ("logLevel", ["Trace", "Info", "Warn", "Error", "Fatal", "None"])).addCallback (&optionChanges.set);
+	opts["logOutput"] = (logOutput = new IntContent ("logOutput")).addCallback (&optionChanges.set);
+	opts["pollInterval"] = (pollInterval = new DoubleContent ("pollInterval")).addCallback (&optionChanges.set);
+	opts["L10n"] = (L10n = new StringContent ("L10n")).addCallback (&optionChanges.set);
+    }
     
     // Overriding validate allows limits to be enforced on variables at load time. Currently
     // there's no infrastructure for enforcing limits when options are set at run-time.
@@ -440,7 +456,7 @@
             logger.warn ("maxThreads must be in the range 1-64. Defaulting to 4.");
             maxThreads = 4;
         }
-        if (pollInterval() !<= 1.0 || pollInterval() !>= 0.0)
+        if (pollInterval() !<= 0.1 || pollInterval() !>= 0.0)
             pollInterval = 0.01;
     }