changeset 80:ea58f277f487

Gui reorganization and changes; partial implementation of floating widgets. Moved contents of mde/gui/WidgetData.d elsewhere; new gui/WidgetDataSet.d and gui/types.d modules. Changes to widget/createWidget.d Partially implemented FloatingAreaWidget to provide an area for floating "window" widgets. New DebugWidget and some uses of it (e.g. bad widget data). Decoupled OptionChanges from Options.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 07 Aug 2008 11:25:27 +0100
parents 61ea26abe4dd
children d8fccaa45d5f 2813ac68576f
files codeDoc/jobs.txt codeDoc/mergetag/new-models.vym data/L10n/OptionsMisc.mtt data/conf/gui.mtt mde/gui/WidgetData.d mde/gui/WidgetDataSet.d mde/gui/WidgetManager.d mde/gui/renderer/IRenderer.d mde/gui/renderer/SimpleRenderer.d mde/gui/types.d mde/gui/widget/Floating.d mde/gui/widget/Ifaces.d mde/gui/widget/Widget.d mde/gui/widget/Window.d mde/gui/widget/createWidget.d mde/gui/widget/miscWidgets.d mde/lookup/Options.d mde/mergetag/Reader.d mde/setup/sdl.d
diffstat 19 files changed, 961 insertions(+), 831 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Tue Aug 05 11:51:51 2008 +0100
+++ b/codeDoc/jobs.txt	Thu Aug 07 11:25:27 2008 +0100
@@ -8,7 +8,7 @@
 
 To do (importance 0-5: 0 pointless, 1 no obvious impact now, 2 todo sometime, 3 useful, 4 important, 5 urgent):
 Also see todo.txt and FIXME/NOTE comment marks.
-4   struct (via tuple) support for parseTo/From. Requires rewriting with static if (is T : type).
+5   setting widgets' default size? setMinSize/setDefaultSize fct?
 4   Try to correlate names of option sections more. (i.e. symbol name, class name, name of i18n translation file)
 4   Not guaranteed to catch up-click ending callback! Appears not to be a problem...
 4   OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
Binary file codeDoc/mergetag/new-models.vym has changed
--- a/data/L10n/OptionsMisc.mtt	Tue Aug 05 11:51:51 2008 +0100
+++ b/data/L10n/OptionsMisc.mtt	Thu Aug 07 11:25:27 2008 +0100
@@ -1,7 +1,7 @@
 {MT01}
 {en-GB}
-<entry|useThreads=["Use threads","Global option for threading in mde."]>
-<entry|logLevel=["Logging level","Controls which messages are logged, from 0=trace to 6=none (default: 1=info)."]>
-<entry|L10n=["Localisation","Specifies the language to use."]>
-<entry|pollInterval=["Polling interval","Delay in main loop to limit CPU usage"]>
-<entry|exitImmediately=["Exit immediately","Load files and exit immediately, without running main loop (for debugging)"]>
+<entry|useThreads={0:"Use threads",1:"Global option for threading in mde."}>
+<entry|logLevel={0:"Logging level",1:"Controls which messages are logged, from 0=trace to 6=none (default: 1=info)."}>
+<entry|L10n={0:"Localisation",1:"Specifies the language to use."}>
+<entry|pollInterval={0:"Polling interval",1:"Delay in main loop to limit CPU usage"}>
+<entry|exitImmediately={0:"Exit immediately",1:"Load files and exit immediately, without running main loop (for debugging)"}>
--- a/data/conf/gui.mtt	Tue Aug 05 11:51:51 2008 +0100
+++ b/data/conf/gui.mtt	Thu Aug 07 11:25:27 2008 +0100
@@ -2,26 +2,13 @@
 <char[]|Renderer="Simple">
 <char[]|Design="Working">
 {Working}
-<WidgetData|root=[0xB004,3,3],["square","blank","square","blank","content","blank","square","blank","square"]>
-<WidgetData|square=[0x1,6,6],[]>
-<WidgetData|content=[0xB004,4,2],["blank","button","blank","blank","blank","opts","blank","blank"]>
-<WidgetData|button=[0x4010,200,200],[]>
-<WidgetData|blank=[0x3001],[]>
-<WidgetData|opts=[0xB005, 0xfe8c00],[]>
+<WidgetData|root={0:[0x8100,3,3],1:["square","blank","square","blank","content","blank","square","blank","square"]}>
+<WidgetData|square={0:[0x1,6,6]}>
+<WidgetData|content={0:[0x8100,4,2],1:["floating","button","blank","blank","blank","opts","blank","blank"]}>
+<WidgetData|button={0:[0x10,200,200]}>
+<WidgetData|blank={0:[0x2]}>
+<WidgetData|opts={0:[0x8110, 0xfe8c00]}>
+<WidgetData|floating={0:[0x8200,20,20],1:["text"]}>
+<WidgetData|text={0:[0x21,0xFF0000],1:["Floating text"]}>
 {Basic}
-<WidgetData|root=[0x21,0x90D970],["A string!"]>
-!{
-{W1}
-<int|x=30>
-<int|y=80>
-<int[][int]|widgetData=[0:[0xB004,2,1,1,2],1:[0x4010,200,200],2:[0x1,100,100]]>
-{W2}
-<int|x=150>
-<int|y=200>
-<int[][int]|widgetData=[0:[0xB004,5,5,3,1,22,1,2,1,1,1,1,1,22,1,22,1,22,1,1,1,1,1,2,1,22,1,3],1:[0x3001],2:[0x21,0,0xFFFF00],3:[0x21,1,0xBFFF00],22:[0x22,1,0xFF00]]>
-<char[][int]|widgetStrings=[0:"alpha=α", 1:"beta=β"]>
-{WEmbedded}
-<int|x=20>
-<int|y=100>
-<int[][int]|widgetData=[0:[0xB005,0,0xB04000]]>
-}
\ No newline at end of file
+<WidgetData|root={0:[0x21,0x90D970],1:["A string!"]}>
--- a/mde/gui/WidgetData.d	Tue Aug 05 11:51:51 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,349 +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/>. */
-
-/*************************************************************************************************
- * Code to manage the data used to create widgets and save changes to it.
- * 
- * When loading, a WidgetDataSet instance is loaded from file and its data used to create the
- * widgets. An empty WidgetDataChanges instance is also created.
- * 
- * If any data requires changing, it is added to the WidgetDataChanges instance, which also
- * updates the the WidgetDataSet instance used to load the widgets (in case of a re-load from this
- * data). When the data should be saved, if the WidgetDataChanges instance is not empty, data from
- * the highest priority (i.e. the user) file is merged into it, preserving both the current
- * changes and previous changes saved to the use file, before saving to the user file.
- *************************************************************************************************/
-module mde.gui.WidgetData;
-
-import mde.gui.exception;
-import mde.gui.widget.Ifaces;
-import mde.gui.widget.createWidget;
-
-// For loading from file:
-import mt = mde.mergetag.DataSet;
-import mt = mde.mergetag.DefaultData;
-import mt = mde.mergetag.exception;
-import mde.mergetag.Reader;
-import mde.mergetag.Writer;
-import mde.setup.paths;
-import mde.mergetag.serialize;
-
-import tango.core.sync.Mutex;
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.gui.WidgetData");
-}
-
-
-/*************************************************************************************************
- * Contains the code for loading and saving the gui, but not the code for drawing it or handling
- * user input.
- * 
- * This abstract class exists solely for separating out some of the functionality.
- *************************************************************************************************/
-abstract scope class WidgetLoader : IWidgetManager
-{
-    /** Construct a new widget loader.
-     * 
-     * params:
-     *  fileName = Name of file specifying the gui, excluding path and extension.
-     */
-    protected this (char[] file) {
-        mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
-        fileName = file;
-    }
-    ~this () {
-        save;
-    }
-    
-    /* Load the widgets' data from the file specified to the CTOR.
-     * 
-     * params:
-     *  allDesigns = Load all sections
-    */
-    private void loadData (bool allDesigns = false) {
-        if (allLoaded || (defaultDesign !is null && allDesigns == false))
-            return; // test if already loaded
-        
-        if (!confDir.exists (fileName)) {
-            logger.error ("Unable to load GUI: no config file!");
-            return; // not a fatal error (so long as the game can run without a GUI!)
-        }
-        
-        // Set up a reader
-        scope IReader reader;
-        try {
-            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true);
-            
-            // Read from the HEADER:
-            // Get the renderer
-            char[]* p = "Renderer" in reader.dataset.header._charA;
-            if (p is null || *p is null) {
-                logger.warn ("No renderer specified: using \"Simple\"");
-                rendName = "Simple";
-            }
-            else
-                rendName = *p;
-            
-            // Get which section to use
-            p = "Design" in reader.dataset.header._charA;
-            if (p is null || *p is null) {
-                logger.warn ("No gui design specified: trying \"Default\"");
-                defaultDesign = "Default";
-            }
-            else
-                defaultDesign = *p;
-            
-            // Read the body:
-            // Load the chosen design
-            reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
-                WidgetDataSet* p = id in data;
-                if (p is null) {
-                    data[id] = new WidgetDataSet;
-                    return *(id in data);
-                }
-                return *p;
-            };
-        
-            if (allDesigns) {
-                reader.read;
-                allLoaded = true;
-            } else
-                reader.read([defaultDesign]);
-        } catch (Exception e) {
-            logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
-            logger.error (e.msg);
-            throw new GuiException ("Failure parsing config file");
-        }
-    }
-    
-    /** Load the gui from some design.
-     * 
-     * If a design was previously loaded, its changes are saved first.
-     * 
-     * Params:
-     *  name = Design to load. If null, the default will be loaded.
-     */
-    void loadDesign (char[] name = null) {
-        if (changes !is null)
-            save;       // own lock
-        
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        
-        // Load data (loadData tests if it's already loaded first):
-        if (name is null) {
-            loadData (false);
-            name = defaultDesign;
-        } else
-            loadData (true);
-        
-        
-        // Get data:
-        curData = data[name]; // NOTE: may throw
-        
-        // Get/create a changes section:
-        if (changesDS is null)
-            changesDS = new mt.DataSet;
-        
-        mt.IDataSection* p = name in changesDS.sec;
-        if (p && ((changes = cast(WidgetDataChanges) *p) !is null)) {}
-        else {
-            changes = new WidgetDataChanges (curData);
-            changesDS.sec[name] = changes;
-        }
-        
-        // Create the widgets:
-        createRootWidget;
-    }
-    
-    /** Save changes, if any exist.
-     * 
-     * Is run when the manager is destroyed, but could be run at other times too. */
-    void save () {
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        
-        // Make all widgets save any changed data; return if no changes:
-        if (!child.saveChanges ("root"))
-            return;
-        
-        if (loadUserFile) { // merge entries from user file into current changes
-            try {
-                scope IReader reader = confDir.makeMTReader (
-                        fileName, PRIORITY.HIGH_ONLY, changesDS, true);
-            
-                // Create if necessary, only corresponding to existing designs read:
-                reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
-                    WidgetDataSet* p = id in data;
-                    if (p is null)
-                        throw new Exception ("File has changed since it was loaded!");
-                    return new WidgetDataChanges (*p);
-                };
-        
-                reader.read;
-            } catch (NoFileException) {
-                // No user file exists; not an error.
-            } catch (Exception e) {
-                logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:");
-                logger.error (e.msg);
-                logger.error ("Overwriting the file.");
-                // Continue...
-            }
-            loadUserFile = false;   // don't need to do it again
-        }
-        
-        try {   // Save
-            IWriter writer;
-            writer = confDir.makeMTWriter (fileName, changesDS);
-            writer.write;
-        } catch (Exception e) {
-            logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:");
-            logger.error (e.msg);
-            // No point in throwing since it doesn't affect anything else.
-        }
-    }
-    
-    /** Get the names of all designs available. */
-    char[][] designs() {
-        synchronized(mutex) {
-            loadData (true);
-            return data.keys;
-        }
-    }
-    
-    
-    /** Create a widget by ID. */
-    IChildWidget makeWidget (widgetID id) {
-        return createWidget (this, curData[id]);
-    }
-    
-    /** For making changes. */
-    void setData (widgetID id, WidgetData d) {
-        changes[id] = d;        // also updates WidgetDataSet in data.
-    }
-    
-    /** Second stage of loading the widgets.
-     * 
-     * loadDesign handles the data; this method needs to:
-     * ---
-     * // 1. Create the root widget:
-     * child = makeWidget ("root");
-     * // 2. Set the setSize, e.g.:
-     * child.setWidth  (child.minWidth,  1);
-     * child.setHeight (child.minHeight, 1);
-     * ---
-     */
-    // FIXME: abstract?
-    void createRootWidget();
-    
-protected:
-    final char[] fileName;
-    char[] defaultDesign;       // The design specified in the file header.
-    char[] rendName;    // Name of renderer; for saving and creating renderers
-    
-    // Loaded data, indexed by design name. May not be loaded for all gui designs:
-    scope WidgetDataSet[char[]] data;
-    private bool allLoaded = false;  // applies to data
-    WidgetDataSet curData;      // Current data
-    WidgetDataChanges changes;  // Changes for the current design.
-    scope mt.DataSet changesDS; // changes and sections from user file (used for saving)
-    bool loadUserFile = true;   // still need to load user file for saving?
-    
-    scope IChildWidget child;   // The primary widget.
-    
-    Mutex mutex;    // lock on methods for use outside the package.
-}
-
-
-package:
-/*************************************************************************************************
- * Contains data for all widgets in a GUI.
- *************************************************************************************************/
-class WidgetDataSet : mt.IDataSection
-{
-    //BEGIN Mergetag code
-    void addTag (char[] tp, mt.ID id, char[] dt) {
-        // Priority is HIGH_LOW. Only load tag if it doesn't already exist.
-        if (tp == "WidgetData" && (id in widgetData) is null) {
-            // Note: is a very simple form of struct deserialization
-            WidgetData data;
-            with(data) {
-                char[][] strs = split (dt);
-                if (strs.length != 2)
-                    throw new ParseException ("Not two components");
-                ints = parseTo!(int[]) (strs[0]);
-                strings = parseTo!(char[][]) (strs[1]);
-            }
-            widgetData[id] = data;
-        }
-    }
-    // Only WidgetDataChanges is used for writing.
-    void writeAll (ItemDelg dlg) {}
-    //END Mergetag code
-    
-    /** Get the widget data for widget i. */
-    WidgetData opIndex (widgetID i) {
-        return widgetData[i];
-    }
-    
-    // Per-widget data:
-    protected WidgetData[widgetID] widgetData;
-}
-
-/*************************************************************************************************
- * Contains changes to widget data.
- * 
- * Can be read as normal and written.
- *************************************************************************************************/
-class WidgetDataChanges : WidgetDataSet
-{
-    /**
-     * Params:
-     *  wds = The base WidgetDataSet these changes are applied against. */
-    this (WidgetDataSet wds) {
-        base = wds;
-    }
-    
-    //BEGIN Mergetag code
-    // HIGH_LOW priority of addTag allows existing entries (i.e. the changes) to be preserved while
-    // other entries are read from files.
-    void writeAll (ItemDelg dlg) {
-        foreach (id,data; widgetData) {
-            // Note: is a very simple form of struct serialization
-            with(data) {
-                dlg ("WidgetData", id, 
-                     parseFrom!(int[]) (ints) ~ ',' ~ parseFrom!(char[][]) (strings) );
-            }
-        }
-    }
-    //END Mergetag code
-    
-    /** Set the widget data for widget i.
-     */
-    void opIndexAssign (WidgetData d, widgetID i) {
-        widgetData[i] = d;
-        base.widgetData[i] = d;
-    }
-    
-    /** Do any changes exist? True if no changes have been stored. */
-    bool none () {
-        return widgetData.length == 0;
-    }
-    
-    protected WidgetDataSet base;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/WidgetDataSet.d	Thu Aug 07 11:25:27 2008 +0100
@@ -0,0 +1,110 @@
+/* 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/>. */
+
+/*************************************************************************************************
+ * Code to manage the data used to create widgets and save changes to it.
+ * 
+ * When loading, a WidgetDataSet instance is loaded from file and its data used to create the
+ * widgets. An empty WidgetDataChanges instance is also created.
+ * 
+ * If any data requires changing, it is added to the WidgetDataChanges instance, which also
+ * updates the the WidgetDataSet instance used to load the widgets (in case of a re-load from this
+ * data). When the data should be saved, if the WidgetDataChanges instance is not empty, data from
+ * the highest priority (i.e. the user) file is merged into it, preserving both the current
+ * changes and previous changes saved to the use file, before saving to the user file.
+ *************************************************************************************************/
+module mde.gui.WidgetDataSet;
+
+public import mde.gui.types;
+
+// For loading from file:
+import mt = mde.mergetag.DataSet;
+import mt = mde.mergetag.DefaultData;
+import mt = mde.mergetag.exception;
+import mde.mergetag.serialize;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.WidgetDataSet");
+}
+
+/*************************************************************************************************
+ * Contains data for all widgets in a GUI.
+ *************************************************************************************************/
+class WidgetDataSet : mt.IDataSection
+{
+    //BEGIN Mergetag code
+    void addTag (char[] tp, mt.ID id, char[] dt) {
+        // Priority is HIGH_LOW. Only load tag if it doesn't already exist.
+        if (tp == "WidgetData" && (id in widgetData) is null) {
+            widgetData[id] = deserialize!(WidgetData) (dt);
+        }
+    }
+    // Only WidgetDataChanges is used for writing.
+    void writeAll (ItemDelg dlg) {}
+    //END Mergetag code
+    
+    /** Get the widget data for widget i. */
+    WidgetData opIndex (widgetID id) {
+        auto p = id in widgetData;
+        if (p is null) {
+            logger.error ("No data for widget "~id~"; creating a debug widget instead.");
+            return WidgetData.dbg;
+        }
+        return *p;
+    }
+    
+    // Per-widget data:
+    protected WidgetData[widgetID] widgetData;
+}
+
+/*************************************************************************************************
+ * Contains changes to widget data.
+ * 
+ * Can be read as normal and written.
+ *************************************************************************************************/
+class WidgetDataChanges : WidgetDataSet
+{
+    /**
+     * Params:
+     *  wds = The base WidgetDataSet these changes are applied against. */
+    this (WidgetDataSet wds) {
+        base = wds;
+    }
+    
+    //BEGIN Mergetag code
+    // HIGH_LOW priority of addTag allows existing entries (i.e. the changes) to be preserved while
+    // other entries are read from files.
+    void writeAll (ItemDelg dlg) {
+        foreach (id,data; widgetData)
+            dlg ("WidgetData", id, serialize!(WidgetData) (data));
+    }
+    //END Mergetag code
+    
+    /** Set the widget data for widget i.
+     */
+    void opIndexAssign (WidgetData d, widgetID i) {
+        widgetData[i] = d;
+        base.widgetData[i] = d;
+    }
+    
+    /** Do any changes exist? True if no changes have been stored. */
+    bool none () {
+        return widgetData.length == 0;
+    }
+    
+    protected WidgetDataSet base;
+}
--- a/mde/gui/WidgetManager.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/WidgetManager.d	Thu Aug 07 11:25:27 2008 +0100
@@ -21,7 +21,7 @@
  *************************************************************************************************/
 module mde.gui.WidgetManager;
 
-public import mde.gui.WidgetData;
+import mde.gui.WidgetDataSet;
 import mde.gui.widget.Ifaces;
 import mde.gui.renderer.createRenderer;
 
@@ -181,3 +181,239 @@
     IRenderer rend;
     wdim w,h;       // area available to the widgets
 }
+
+
+import mde.gui.exception;
+import mde.gui.widget.Ifaces;
+import mde.gui.widget.createWidget;
+
+import mde.mergetag.Reader;
+import mde.mergetag.Writer;
+import mde.setup.paths;
+
+/*************************************************************************************************
+* Contains the code for loading and saving the gui, but not the code for drawing it or handling
+* user input.
+* 
+* This abstract class exists solely for separating out some of the functionality.
+*************************************************************************************************/
+abstract scope class WidgetLoader : IWidgetManager
+{
+    /** Construct a new widget loader.
+    * 
+    * params:
+    *  fileName = Name of file specifying the gui, excluding path and extension.
+    */
+    protected this (char[] file) {
+        mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
+        fileName = file;
+    }
+    ~this () {
+        save;
+    }
+    
+    /* Load the widgets' data from the file specified to the CTOR.
+    * 
+    * params:
+    *  allDesigns = Load all sections
+    */
+    private void loadData (bool allDesigns = false) {
+        if (allLoaded || (defaultDesign !is null && allDesigns == false))
+            return; // test if already loaded
+            
+            if (!confDir.exists (fileName)) {
+                logger.error ("Unable to load GUI: no config file!");
+                return; // not a fatal error (so long as the game can run without a GUI!)
+            }
+            
+            // Set up a reader
+            scope IReader reader;
+        try {
+            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true);
+            
+            // Read from the HEADER:
+            // Get the renderer
+            char[]* p = "Renderer" in reader.dataset.header._charA;
+            if (p is null || *p is null) {
+                logger.warn ("No renderer specified: using \"Simple\"");
+                rendName = "Simple";
+            }
+            else
+                rendName = *p;
+            
+            // Get which section to use
+            p = "Design" in reader.dataset.header._charA;
+            if (p is null || *p is null) {
+                logger.warn ("No gui design specified: trying \"Default\"");
+                defaultDesign = "Default";
+            }
+            else
+                defaultDesign = *p;
+            
+            // Read the body:
+            // Load the chosen design
+            reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
+                WidgetDataSet* p = id in data;
+                if (p is null) {
+                    data[id] = new WidgetDataSet;
+                    return *(id in data);
+                }
+                return *p;
+            };
+            
+            if (allDesigns) {
+                reader.read;
+                allLoaded = true;
+            } else
+                reader.read([defaultDesign]);
+        } catch (Exception e) {
+            logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
+            logger.error (e.msg);
+            throw new GuiException ("Failure parsing config file");
+        }
+    }
+    
+    /** Load the gui from some design.
+    * 
+    * If a design was previously loaded, its changes are saved first.
+    * 
+    * Params:
+    *  name = Design to load. If null, the default will be loaded.
+    */
+    void loadDesign (char[] name = null) {
+        if (changes !is null)
+            save;       // own lock
+            
+            mutex.lock;
+        scope(exit) mutex.unlock;
+        
+        // Load data (loadData tests if it's already loaded first):
+        if (name is null) {
+            loadData (false);
+            name = defaultDesign;
+        } else
+            loadData (true);
+        
+        
+        // Get data:
+        auto p = name in data;
+        while (p is null) {
+            if (name == defaultDesign)
+                throw new GuiException ("Unable to load [specified or] default design");
+            name = defaultDesign;       // try again with the default
+            p = name in data;
+        }
+        curData = *p;
+        
+        // Get/create a changes section:
+        if (changesDS is null)
+            changesDS = new mt.DataSet;
+        
+        mt.IDataSection* q = name in changesDS.sec;
+        if (q && ((changes = cast(WidgetDataChanges) *q) !is null)) {}
+        else {
+            changes = new WidgetDataChanges (curData);
+            changesDS.sec[name] = changes;
+        }
+        
+        // Create the widgets:
+        createRootWidget;
+    }
+    
+    /** Save changes, if any exist.
+    * 
+    * Is run when the manager is destroyed, but could be run at other times too. */
+    void save () {
+        mutex.lock;
+        scope(exit) mutex.unlock;
+        
+        // Make all widgets save any changed data; return if no changes:
+        if (!child.saveChanges ("root"))
+            return;
+        
+        if (loadUserFile) { // merge entries from user file into current changes
+            try {
+                scope IReader reader = confDir.makeMTReader (
+                fileName, PRIORITY.HIGH_ONLY, changesDS, true);
+                
+                // Create if necessary, only corresponding to existing designs read:
+                reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
+                    WidgetDataSet* p = id in data;
+                    if (p is null)
+                        throw new Exception ("File has changed since it was loaded!");
+                    return new WidgetDataChanges (*p);
+                };
+                
+                reader.read;
+            } catch (NoFileException) {
+                // No user file exists; not an error.
+            } catch (Exception e) {
+                logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:");
+                logger.error (e.msg);
+                logger.error ("Overwriting the file.");
+                // Continue...
+            }
+            loadUserFile = false;   // don't need to do it again
+        }
+        
+        try {   // Save
+        IWriter writer;
+        writer = confDir.makeMTWriter (fileName, changesDS);
+        writer.write;
+        } catch (Exception e) {
+            logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:");
+            logger.error (e.msg);
+            // No point in throwing since it doesn't affect anything else.
+        }
+    }
+    
+    /** Get the names of all designs available. */
+    char[][] designs() {
+        synchronized(mutex) {
+            loadData (true);
+            return data.keys;
+        }
+    }
+    
+    
+    /** Create a widget by ID. */
+    IChildWidget makeWidget (widgetID id, IParentWidget parent = null) {
+        debug logger.trace ("Creating widget \""~id~'"');
+        return createWidget (this, curData[id], parent);
+    }
+    
+    /** For making changes. */
+    void setData (widgetID id, WidgetData d) {
+        changes[id] = d;        // also updates WidgetDataSet in data.
+    }
+    
+    /** Second stage of loading the widgets.
+    * 
+    * loadDesign handles the data; this method needs to:
+    * ---
+    * // 1. Create the root widget:
+    * child = makeWidget ("root");
+    * // 2. Set the setSize, e.g.:
+    * child.setWidth  (child.minWidth,  1);
+    * child.setHeight (child.minHeight, 1);
+    * ---
+    */
+    void createRootWidget();
+    
+    protected:
+        final char[] fileName;
+        char[] defaultDesign;       // The design specified in the file header.
+        char[] rendName;    // Name of renderer; for saving and creating renderers
+        
+        // Loaded data, indexed by design name. May not be loaded for all gui designs:
+        scope WidgetDataSet[char[]] data;
+        private bool allLoaded = false;  // applies to data
+        WidgetDataSet curData;      // Current data
+        WidgetDataChanges changes;  // Changes for the current design.
+        scope mt.DataSet changesDS; // changes and sections from user file (used for saving)
+        bool loadUserFile = true;   // still need to load user file for saving?
+        
+        scope IChildWidget child;   // The primary widget.
+        
+        Mutex mutex;    // lock on methods for use outside the package.
+}
--- a/mde/gui/renderer/IRenderer.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/renderer/IRenderer.d	Thu Aug 07 11:25:27 2008 +0100
@@ -16,8 +16,7 @@
 /** Interface for the renderer. This is planned to replace decoration.d */
 module mde.gui.renderer.IRenderer;
 
-// Put here to avoid circular import.
-typedef int wdim;
+public import mde.gui.types;
 
 /** Interface for renderers.
 *
@@ -71,6 +70,15 @@
     //END Get dimensions
     
     //BEGIN draw routines
+    /** Restrict following draw operations to given box.
+     *
+     * Restrict "pushes" a restriction onto a stack; relax must be called afterwards to "pop" the
+     * restriction. */
+    void restrict (wdim x, wdim y, wdim w, wdim h);
+    
+    /** See restrict. */
+    void relax ();
+    
     /** Draw a window border plus background. */
     void drawWindow (wdim x, wdim y, wdim w, wdim h);
     
--- a/mde/gui/renderer/SimpleRenderer.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/renderer/SimpleRenderer.d	Thu Aug 07 11:25:27 2008 +0100
@@ -79,6 +79,10 @@
     }
     
     
+    //FIXME - make these do something
+    void restrict (wdim x, wdim y, wdim w, wdim h) {}
+    void relax () {}
+    
     void drawWindow (wdim x, wdim y, wdim w, wdim h) {
         gl.setColor (0f, 0f, .7f);
         gl.drawBox (x,y, w,h);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/types.d	Thu Aug 07 11:25:27 2008 +0100
@@ -0,0 +1,64 @@
+/* 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/>. */
+
+/*************************************************************************************************
+ * Basic data types used by widgets.
+ *************************************************************************************************/
+module mde.gui.types;
+
+
+/** Widget ID type. Each ID is unique under this window.
+*
+* Type is int since this is the widget data type. */
+alias char[] widgetID;
+
+/** Window coordinate and dimension/size type (int).
+*
+* Used to disambiguate between general integers and coordinates; all widget positions/sizes should
+* use this type (or one of the aliases below).
+* 
+* Aliases of wdim providing extra information about what their contents hold: absolute position,
+* position relative to the containing widget (wdrel should not be used if relative to anything
+* else), or size. Their use instead of wdim is optional (and in some cases wdim values aren't of
+* any of these types). Also don't use these aliases for variables which may also be used to other
+* effects, e.g. if they can have special values with special meanings. */
+typedef int     wdim;
+alias   wdim    wdabs;  /// ditto
+alias   wdim    wdrel;  /// ditto
+alias   wdim    wdsize; /// ditto
+
+/** A pair of wdim variables, and strictly no other data (methods may be added if deemed useful).
+*
+* Potentially usable to return two wdim variables, e.g. width and height, from a function.
+* However, the current usage of out variables looks like it's better. */
+struct wdimPair {
+    wdim x, y;  /// data
+}
+
+
+/*************************************************************************************************
+* The data type all widgets creatable by the widget manager receive on creation.
+* 
+* Conversion code to/from MT tags is contained in the addTag and writeAll methods of
+* WidgetDataSet and WidgetDataChanges.
+*************************************************************************************************/
+struct WidgetData
+{
+    int[]    ints;      /// Integer data
+    char[][] strings;   /// char[] data
+    
+    /** For creating a DebugWidget. */
+    static WidgetData dbg = { ints : [0xF] };   // 0xf is current code for debug widget
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/widget/Floating.d	Thu Aug 07 11:25:27 2008 +0100
@@ -0,0 +1,412 @@
+/* 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 Window class. Becoming a widget. */
+module mde.gui.widget.Floating;
+
+import mde.gui.widget.Widget;
+import mde.gui.exception;
+/+import mde.gui.widget.createWidget;
+
+import mde.gui.IGui;
+import mde.gui.renderer.createRenderer;
+
+import mt = mde.mergetag.DataSet;
+import mde.mergetag.parse.parseTo : parseTo;
+import mde.mergetag.parse.parseFrom : parseFrom;
++/
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.widget.Window");
+}
+//FIXME - documentation
+
+/** GUI Window class
+ *
+ * A window class instance does two things: (1) specify a region of the screen upon which the window
+ * and its associated widgets are drawn, and (2) load, save, and generally manage all its widgets.
+ *
+ * Let the window load a table of widget data, of type int[][widgetID]. Each widget will, when
+ * created, be given its int[] of data, which this() must confirm is valid (or throw).
+ */
+/** An area to contain floating widgets.
+ *
+ * The position of each sub-widget is set from data, but not the size.
+ * Rationale: parents' need to set subwidgets' positions when its position is set, so it needs to
+ * know their positions. Size setting is still under work FIXME.
+ *
+ * Data: Each string item is interpreted as a subwidget widgetID.
+ * Ints supplied may consist of just the widget type or
+ * additionally an (x,y) coordinate for each subwidget (all x coords first, then all y coords).
+ */
+class FloatingAreaWidget : SizableWidget, IParentWidget
+{
+    this (IWidgetManager mgr, WidgetData data) {
+        subWidgets.length = data.strings.length;
+        foreach (i,s; data.strings)
+            subWidgets[i] = mgr.makeWidget (s, this);
+        sWCoords.length = subWidgets.length;
+        
+        if (data.ints.length != 1) {
+            if (data.ints.length != 2*subWidgets.length + 1) {
+                throw new WidgetDataException;
+            }
+            foreach (i, ref c; sWCoords) {
+                c.x = cast(wdim) data.ints[i + 1];
+                c.y = cast(wdim) data.ints[i + 1 + sWCoords.length];
+            }
+        }
+        
+        super (mgr, data);
+        
+        foreach (w; subWidgets) {
+            //FIXME: set default size
+            w.setWidth  (w.minWidth, -1);
+            w.setHeight (w.minHeight, -1);
+        }
+    }
+    
+    void setPosition (wdim x, wdim y) {
+        super.setPosition (x,y);
+        
+        foreach (i,c; sWCoords)
+            subWidgets[i].setPosition (x+c.x, y+c.y);
+    }
+    
+    void draw () {
+        super.draw;
+        
+        mgr.renderer.restrict (x,y, w,h);
+        
+        foreach (w; subWidgets)
+            w.draw;
+    }
+    
+protected:
+    IChildWidget[] subWidgets;  // all subwidgets, framed or not
+    wdimPair[] sWCoords;        // coords for subwidgets, relative to this widget
+    
+    /+
+    /** Call after loading is finished to setup the window and confirm that it's valid.
+     *
+     * Throws: WindowLoadException (or possibly other exceptions). Do not use the instance if an
+     * exception occurs! */
+    void finalise (IGui gui)
+    in {
+        assert (gui !is null, "Window.finalise ("~name~"): gui is null");
+    } body {
+        // Check data was loaded:
+        if (widgetData is null)
+            throw new WindowLoadException ("No widget creation data");
+        
+        // Save gui and create the renderer:
+        gui_ = gui;
+        rend = createRenderer (gui.rendererName);
+        
+        // Create the primary widget (and indirectly all sub-widgets), throwing on error:
+        // Note: GridLayoutWidget's this relies on rend.layoutSpacing.
+        widget = makeWidget (0);        // primary widget always has ID 0.
+        // This data is no longer needed by Window, although its sub-arrays may still be used, so
+        // let the GC collect what it can:
+        widgetData = null;
+        widgetStrings = null;
+        
+        // get border sizes:
+        border = rend.setSizable (isWSizable, isHSizable);  // depends on widget
+        
+        // Note: this should return an empty array, but we shouldn't make a fuss if it's not empty:
+        if ((widget.adjust (mutableData)).length != 0)	// adjust/set size, etc., depends on rend
+            logger.warn ("Local widget position data is invalid!");
+        mutableData = null;             // no longer needed
+        
+        widget.getCurrentSize (w,h);    // and get this size
+        w += border.l + border.r;       // Adjust for border
+        h += border.t + border.b;
+        
+        widgetX = x + border.l;         // widget position
+        widgetY = y + border.t;         // must be updated if the window is moved
+        widget.setPosition (widgetX, widgetY);
+        
+        // Calculate mw/mh and xw/yh (cached data):
+        mw = widget.minWidth  + border.l + border.r;
+        mh = widget.minHeight + border.t + border.b;
+        
+        xw = x+w;
+        yh = y+h;
+    }
+    //END Methods for GUI
+    
+    //BEGIN IWindow methods
+    /** Get/create a widget by ID.
+     *
+     * Should $(I only) be called internally and by sub-widgets! */
+    IWidget makeWidget (widgetID i)
+    in {
+        // widgetData is normally left to be garbage collected after widgets have been created:
+        assert (widgetData !is null, "Window.makeWidget ("~name~"): widgetData is null");
+    } body {
+        /* Each widget returned should be a unique object; if multiple widgets are requested with
+        * the same ID, a new widget is created each time. */
+        
+        int[]* d = i in widgetData;
+        if (d is null)
+            throw new WindowLoadException ("Window.makeWidget ("~name~"): Widget not found");
+        
+        // Throws WidgetDataException (a WindowLoadException) if bad data:
+        return createWidget (this, *d);
+    }
+    
+    char[] getWidgetString (int i)
+    in {
+        // widgetStrings is freed at same time as widgetData
+        // but widgetData is guaranteed to be read
+        assert (widgetData !is null, "getWidgetString called after widget creation finished");
+    } body {
+        char[]* p;
+        if (widgetStrings is null ||
+             (p = i in widgetStrings) is null )
+            throw new WindowLoadException ("Needed widgetStrings not set for Window");
+        
+        return *p;
+    }
+    
+    /** Add this widget's data to that to be saved, returning it's widgetID. */
+    widgetID addCreationData (IWidget widget)
+    {
+        widgetID i;
+        if (widgetData is null)
+            i = 0;
+        else
+            i = widgetData.keys[$-1] + 1;
+        
+        /+ Doesn't this have no effect except when getCreationData throws, in which case the data
+         + isn't used anyway? I'm sure it was added for a reason... FIXME and below
+        widgetData[i] = null;   // Make sure the same ID doesn't get used by a recursive call
+        +/
+        widgetData[i] = widget.getCreationData;
+        
+        return i;
+    }
+    
+    int addWidgetString (char[] str)
+    {
+        int i;
+        if (widgetStrings is null)
+            i = 0;
+        else
+            i = widgetStrings.keys[$-1] + 1;
+        
+        /+ See above. FIXME
+        widgetStrings[i] = null;   // Make sure the same ID doesn't get used by a recursive call
+        +/
+        widgetStrings[i] = str;
+        
+        return i;
+    }
+    
+    IGui gui () {
+        return gui_;
+    }
+    
+    void requestRedraw () {
+        gui_.requestRedraw;
+    }
+    
+    IRenderer renderer ()
+    in {
+        assert (rend !is null, "Window.renderer: rend is null");
+    } body {
+        return rend;
+    }
+    //END IWindow methods
+    
+    //BEGIN IWidget methods
+    //FIXME: how many of these methods are actually needed/used?
+    int[] adjust (int[]) {          // simply not relevant (never used)
+        return [];
+    }
+    int[] getCreationData () {      // simply not relevant (never used)
+        return [];
+    }
+    int[] getMutableData () {       // simply not relevant (never used)
+        return [];
+    }
+    
+    bool isWSizable () {
+        return widget.isWSizable;
+    }
+    bool isHSizable () {
+        return widget.isHSizable;
+    }
+    
+    wdim minWidth () {
+        return mw;
+    }
+    wdim minHeight () {
+        return mh;
+    }
+    void getCurrentSize (out wdim cw, out wdim ch) {
+        cw = w;
+        ch = h;
+    }
+    
+    void setWidth (wdim nw, int dir) {
+        if (nw < mw) w = mw;	// clamp
+        else w = nw;
+        xw = x + w;
+        widget.setWidth (w - border.l - border.r, dir);
+    }
+    void setHeight (wdim nh, int dir) {
+        if (nh < mh) h = mh;	// clamp
+        else h = nh;
+        yh = y + h;
+        widget.setHeight (h - border.t - border.b, dir);
+    }
+    
+    void setPosition (wdim nx, wdim ny) {
+        x = nx;
+        y = ny;
+        
+        xw = x+w;
+        yh = y+h;
+        
+        widgetX = x + border.l;
+        widgetY = y + border.t;
+        
+        widget.setPosition (widgetX, widgetY);
+        
+        gui_.requestRedraw ();  // necessary whenever the window is moved; setPosition is called after resizes and moves
+    }
+    
+    IWidget getWidget (wdim cx, wdim cy) {
+        if (cx < x || cx >= xw || cy < y || cy >= yh)   // not over window
+            return null;
+        if (cx >= widgetX && cx < xw-border.r && cy >= widgetY && cy < yh-border.b)
+                                                        // over the widget
+            return widget.getWidget (cx, cy);
+        else                                            // over the window border
+            return this;
+    }
+    void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) {
+        if (b == 1 && state == true) {
+            resizeType = rend.getResizeType (cx-x, cy-y, w,h);
+            
+            if (resizeType != RESIZE_TYPE.NONE) {    // Some type of resize
+                // Set x/yDrag (unfortunately these need to be different for each edge)
+                if (resizeType & RESIZE_TYPE.L)
+                    xDrag = w + cx;
+                else if (resizeType & RESIZE_TYPE.R)
+                    xDrag = w - cx;
+                
+                if (resizeType & RESIZE_TYPE.T)
+                    yDrag = h + cy;
+                else if (resizeType & RESIZE_TYPE.B)
+                    yDrag = h - cy;
+                
+                // Add the callbacks (they use resizeType which has already been set)
+                gui_.addClickCallback (&endCallback);
+                gui_.addMotionCallback (&resizeCallback);
+            } else {                             // window is being moved
+                xDrag = cx - x;
+                yDrag = cy - y;
+                
+                gui_.addClickCallback (&endCallback);
+                gui_.addMotionCallback (&moveCallback);
+            }
+        }
+    }
+    
+    void draw () {
+        // background
+        rend.drawWindow (x,y, w,h);
+        
+        // Tell the widget to draw itself:
+        widget.draw();
+    }
+    //END IWidget methods
+    
+private:
+    alias IRenderer.BorderDimensions BorderDimensions;
+    alias IRenderer.RESIZE_TYPE RESIZE_TYPE;
+    
+    //BEGIN Window moving and resizing
+    void moveCallback (wdabs cx, wdabs cy) {
+        setPosition (cx-xDrag, cy-yDrag);
+    }
+    void resizeCallback (wdabs cx, wdabs cy) {
+        debug scope(failure)
+                logger.trace ("resizeCallback: failure");
+        
+        // This function is only called if some resize is going to happen.
+        // x,y are used as parameters to setPosition as well as being affected by it; somewhat
+        // pointless but fairly effective.
+        
+        if (resizeType & RESIZE_TYPE.L) {
+            wdim xSize = xDrag - cx;
+            if (xSize < mw) xSize = mw;	// clamp
+            x += w - xSize;
+            setWidth (xSize, 1);
+        }
+        else if (resizeType & RESIZE_TYPE.R) {
+            setWidth (xDrag + cx, -1);
+        }
+        if (resizeType & RESIZE_TYPE.T) {
+            wdim ySize = yDrag - cy;
+            if (ySize < mh) ySize = mh;
+            y += h - ySize;
+            setHeight (ySize, 1);
+        }
+        else if (resizeType & RESIZE_TYPE.B) {
+            setHeight (yDrag + cy, -1);
+        }
+        
+        // Moves lower (x,y) coordinate if necessary and repositions any sub-widgets moved by the
+        // resizing:
+        setPosition (x, y);
+    }
+    bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
+        if (b == 1 && state == false) {
+            // The mouse shouldn't have moved since the motion callback
+            // was last called, so there's nothing else to do now.
+            gui_.removeCallbacks (cast(void*) this);
+            
+            return true;    // we've handled the up-click
+        }
+        return false;       // we haven't handled it
+    }
+    wdim xDrag, yDrag;              // where a drag starts relative to x and y
+    IRenderer.RESIZE_TYPE resizeType;   // Type of current resize
+    //END Window moving and resizing
+    
+    // Load/save data:
+    public char[] name;             // The window's name (id from config file)
+    //bool edited = false;            // True if any widgets have been edited (excluding scaling)
+    
+    IGui gui_;                      // The gui managing this window
+    IRenderer rend;                 // The window's renderer
+    
+    IWidget widget;                 // The primary widget in this window.
+    
+    wdim x = -1, y = -1;            // Window position
+    wdsize w,h;                     // Window size (calculated from Widgets)
+    wdim xw, yh;                    // x+w, y+h (frequent use by clickEvent)
+    wdim widgetX, widgetY;          // Widget position (= window position plus BORDER_WIDTH)
+    wdim mw = -1, mh = -1;          // minimal size (negative means requires calculation)
+    
+    BorderDimensions border;        // Total border size (move plus resize)
+    +/
+}
--- a/mde/gui/widget/Ifaces.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/widget/Ifaces.d	Thu Aug 07 11:25:27 2008 +0100
@@ -23,41 +23,10 @@
  *************************************************************************************************/
 module mde.gui.widget.Ifaces;
 
+public import mde.gui.types;
 public import mde.gui.renderer.IRenderer;
 
 
-/** Widget ID type. Each ID is unique under this window.
- *
- * Type is int since this is the widget data type. */
-alias char[] widgetID;
-
-/** Window coordinate and dimension/size type (int).
- *
- * Used to disambiguate between general integers and coordinates; all widget positions/sizes should
- * use this type (or one of the aliases below).
- * 
- * ---
- * typedef int wdim;    // Declared in IRenderer to avoid a circular import.
- * ---
- * 
- * Aliases of wdim providing extra information about what their contents hold: absolute position,
- * position relative to the containing widget (wdrel should not be used if relative to anything
- * else), or size. Their use instead of wdim is optional (and in some cases wdim values aren't of
- * any of these types). Also don't use these aliases for variables which may also be used to other
- * effects, e.g. if they can have special values with special meanings. */
-alias wdim	wdabs;
-alias wdim	wdrel;	/// ditto
-alias wdim	wdsize;	/// ditto
-
-/** A pair of wdim variables, and strictly no other data (methods may be added if deemed useful).
- *
- * Potentially usable to return two wdim variables, e.g. width and height, from a function.
- * However, the current usage of out variables looks like it's better. */
-struct wdimPair {
-    wdim x, y;	/// data
-}
-
-
 /*************************************************************************************************
  * Common interface for all widgets.
  *
@@ -82,12 +51,16 @@
     // Loading/saving:
     /** Create a widget by ID.
      *
+     * Params:
+     *  id      = Identifier, within data files, of the data for the widget.
+     *  parent  = Reference to the widget's parent, passed if the widget supports recieving it.
+     *
      * Creates a widget, using the widget data with index id. Widget data is loaded from files,
      * and per design (multiple gui layouts, called designs, may exist; data is per design).
      * 
-     * Note: this method is only for "named" widgets; generic widgets instanciated in lists are
-     * created differently. */
-    IChildWidget makeWidget (widgetID id);
+     * Note: this method is only for widgets with an identifier; generic widgets instanciated in
+     * lists are created directly and have no WidgetData of their own. */
+    IChildWidget makeWidget (widgetID id, IParentWidget parent = null);
     
     /** Record some changes, for saving. */
     void setData (widgetID id, WidgetData);
@@ -266,16 +239,3 @@
      * part of the widget is visible: scroll bars or a hidden window. */
     void draw ();
 }
-
-
-/*************************************************************************************************
- * The data type all widgets creatable by the widget manager receive on creation.
- * 
- * Conversion code to/from MT tags is contained in the addTag and writeAll methods of
- * WidgetDataSet and WidgetDataChanges.
- *************************************************************************************************/
-struct WidgetData
-{
-    int[]    ints;
-    char[][] strings;
-}
--- a/mde/gui/widget/Widget.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/widget/Widget.d	Thu Aug 07 11:25:27 2008 +0100
@@ -23,7 +23,6 @@
 module mde.gui.widget.Widget;
 
 public import mde.gui.widget.Ifaces;
-import mde.gui.renderer.IRenderer;
 import mde.gui.exception;
 
 
--- a/mde/gui/widget/Window.d	Tue Aug 05 11:51:51 2008 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,360 +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 Window class. Hopefully eventually this will become a widget to make things a bit more
-* generic. */
-module mde.gui.widget.Window;
-
-import mde.gui.widget.Ifaces;
-import mde.gui.widget.createWidget;
-
-import mde.gui.IGui;
-import mde.gui.exception;
-import mde.gui.renderer.createRenderer;
-
-import mt = mde.mergetag.DataSet;
-import mde.mergetag.parse.parseTo : parseTo;
-import mde.mergetag.parse.parseFrom : parseFrom;
-
-import tango.util.log.Log : Log, Logger;
-
-private Logger logger;
-static this () {
-    logger = Log.getLogger ("mde.gui.widget.Window");
-}
-
-/** GUI Window class
- *
- * A window class instance does two things: (1) specify a region of the screen upon which the window
- * and its associated widgets are drawn, and (2) load, save, and generally manage all its widgets.
- *
- * Let the window load a table of widget data, of type int[][widgetID]. Each widget will, when
- * created, be given its int[] of data, which this() must confirm is valid (or throw).
- */
-class Window : mt.IDataSection, IWindow
-{
-    //BEGIN Methods for GUI
-    this (char[] id) {
-        name = id;
-    }
-    
-    /** Call after loading is finished to setup the window and confirm that it's valid.
-     *
-     * Throws: WindowLoadException (or possibly other exceptions). Do not use the instance if an
-     * exception occurs! */
-    void finalise (IGui gui)
-    in {
-        assert (gui !is null, "Window.finalise ("~name~"): gui is null");
-    } body {
-        // Check data was loaded:
-        if (widgetData is null)
-            throw new WindowLoadException ("No widget creation data");
-        
-        // Save gui and create the renderer:
-        gui_ = gui;
-        rend = createRenderer (gui.rendererName);
-        
-        // Create the primary widget (and indirectly all sub-widgets), throwing on error:
-        // Note: GridLayoutWidget's this relies on rend.layoutSpacing.
-        widget = makeWidget (0);        // primary widget always has ID 0.
-        // This data is no longer needed by Window, although its sub-arrays may still be used, so
-        // let the GC collect what it can:
-        widgetData = null;
-        widgetStrings = null;
-        
-        // get border sizes:
-        border = rend.setSizable (isWSizable, isHSizable);  // depends on widget
-        
-        // Note: this should return an empty array, but we shouldn't make a fuss if it's not empty:
-        if ((widget.adjust (mutableData)).length != 0)	// adjust/set size, etc., depends on rend
-            logger.warn ("Local widget position data is invalid!");
-        mutableData = null;             // no longer needed
-        
-        widget.getCurrentSize (w,h);    // and get this size
-        w += border.l + border.r;       // Adjust for border
-        h += border.t + border.b;
-        
-        widgetX = x + border.l;         // widget position
-        widgetY = y + border.t;         // must be updated if the window is moved
-        widget.setPosition (widgetX, widgetY);
-        
-        // Calculate mw/mh and xw/yh (cached data):
-        mw = widget.minWidth  + border.l + border.r;
-        mh = widget.minHeight + border.t + border.b;
-        
-        xw = x+w;
-        yh = y+h;
-    }
-    //END Methods for GUI
-    
-    //BEGIN IWindow methods
-    /** Get/create a widget by ID.
-     *
-     * Should $(I only) be called internally and by sub-widgets! */
-    IWidget makeWidget (widgetID i)
-    in {
-        // widgetData is normally left to be garbage collected after widgets have been created:
-        assert (widgetData !is null, "Window.makeWidget ("~name~"): widgetData is null");
-    } body {
-        /* Each widget returned should be a unique object; if multiple widgets are requested with
-        * the same ID, a new widget is created each time. */
-        
-        int[]* d = i in widgetData;
-        if (d is null)
-            throw new WindowLoadException ("Window.makeWidget ("~name~"): Widget not found");
-        
-        // Throws WidgetDataException (a WindowLoadException) if bad data:
-        return createWidget (this, *d);
-    }
-    
-    char[] getWidgetString (int i)
-    in {
-        // widgetStrings is freed at same time as widgetData
-        // but widgetData is guaranteed to be read
-        assert (widgetData !is null, "getWidgetString called after widget creation finished");
-    } body {
-        char[]* p;
-        if (widgetStrings is null ||
-             (p = i in widgetStrings) is null )
-            throw new WindowLoadException ("Needed widgetStrings not set for Window");
-        
-        return *p;
-    }
-    
-    /** Add this widget's data to that to be saved, returning it's widgetID. */
-    widgetID addCreationData (IWidget widget)
-    {
-        widgetID i;
-        if (widgetData is null)
-            i = 0;
-        else
-            i = widgetData.keys[$-1] + 1;
-        
-        /+ Doesn't this have no effect except when getCreationData throws, in which case the data
-         + isn't used anyway? I'm sure it was added for a reason... FIXME and below
-        widgetData[i] = null;   // Make sure the same ID doesn't get used by a recursive call
-        +/
-        widgetData[i] = widget.getCreationData;
-        
-        return i;
-    }
-    
-    int addWidgetString (char[] str)
-    {
-        int i;
-        if (widgetStrings is null)
-            i = 0;
-        else
-            i = widgetStrings.keys[$-1] + 1;
-        
-        /+ See above. FIXME
-        widgetStrings[i] = null;   // Make sure the same ID doesn't get used by a recursive call
-        +/
-        widgetStrings[i] = str;
-        
-        return i;
-    }
-    
-    IGui gui () {
-        return gui_;
-    }
-    
-    void requestRedraw () {
-        gui_.requestRedraw;
-    }
-    
-    IRenderer renderer ()
-    in {
-        assert (rend !is null, "Window.renderer: rend is null");
-    } body {
-        return rend;
-    }
-    //END IWindow methods
-    
-    //BEGIN IWidget methods
-    //FIXME: how many of these methods are actually needed/used?
-    int[] adjust (int[]) {          // simply not relevant (never used)
-        return [];
-    }
-    int[] getCreationData () {      // simply not relevant (never used)
-        return [];
-    }
-    int[] getMutableData () {       // simply not relevant (never used)
-        return [];
-    }
-    
-    bool isWSizable () {
-        return widget.isWSizable;
-    }
-    bool isHSizable () {
-        return widget.isHSizable;
-    }
-    
-    wdim minWidth () {
-        return mw;
-    }
-    wdim minHeight () {
-        return mh;
-    }
-    void getCurrentSize (out wdim cw, out wdim ch) {
-        cw = w;
-        ch = h;
-    }
-    
-    void setWidth (wdim nw, int dir) {
-        if (nw < mw) w = mw;	// clamp
-        else w = nw;
-        xw = x + w;
-        widget.setWidth (w - border.l - border.r, dir);
-    }
-    void setHeight (wdim nh, int dir) {
-        if (nh < mh) h = mh;	// clamp
-        else h = nh;
-        yh = y + h;
-        widget.setHeight (h - border.t - border.b, dir);
-    }
-    
-    void setPosition (wdim nx, wdim ny) {
-        x = nx;
-        y = ny;
-        
-        xw = x+w;
-        yh = y+h;
-        
-        widgetX = x + border.l;
-        widgetY = y + border.t;
-        
-        widget.setPosition (widgetX, widgetY);
-        
-        gui_.requestRedraw ();  // necessary whenever the window is moved; setPosition is called after resizes and moves
-    }
-    
-    IWidget getWidget (wdim cx, wdim cy) {
-        if (cx < x || cx >= xw || cy < y || cy >= yh)   // not over window
-            return null;
-        if (cx >= widgetX && cx < xw-border.r && cy >= widgetY && cy < yh-border.b)
-                                                        // over the widget
-            return widget.getWidget (cx, cy);
-        else                                            // over the window border
-            return this;
-    }
-    void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) {
-        if (b == 1 && state == true) {
-            resizeType = rend.getResizeType (cx-x, cy-y, w,h);
-            
-            if (resizeType != RESIZE_TYPE.NONE) {    // Some type of resize
-                // Set x/yDrag (unfortunately these need to be different for each edge)
-                if (resizeType & RESIZE_TYPE.L)
-                    xDrag = w + cx;
-                else if (resizeType & RESIZE_TYPE.R)
-                    xDrag = w - cx;
-                
-                if (resizeType & RESIZE_TYPE.T)
-                    yDrag = h + cy;
-                else if (resizeType & RESIZE_TYPE.B)
-                    yDrag = h - cy;
-                
-                // Add the callbacks (they use resizeType which has already been set)
-                gui_.addClickCallback (&endCallback);
-                gui_.addMotionCallback (&resizeCallback);
-            } else {                             // window is being moved
-                xDrag = cx - x;
-                yDrag = cy - y;
-                
-                gui_.addClickCallback (&endCallback);
-                gui_.addMotionCallback (&moveCallback);
-            }
-        }
-    }
-    
-    void draw () {
-        // background
-        rend.drawWindow (x,y, w,h);
-        
-        // Tell the widget to draw itself:
-        widget.draw();
-    }
-    //END IWidget methods
-    
-private:
-    alias IRenderer.BorderDimensions BorderDimensions;
-    alias IRenderer.RESIZE_TYPE RESIZE_TYPE;
-    
-    //BEGIN Window moving and resizing
-    void moveCallback (wdabs cx, wdabs cy) {
-        setPosition (cx-xDrag, cy-yDrag);
-    }
-    void resizeCallback (wdabs cx, wdabs cy) {
-        debug scope(failure)
-                logger.trace ("resizeCallback: failure");
-        
-        // This function is only called if some resize is going to happen.
-        // x,y are used as parameters to setPosition as well as being affected by it; somewhat
-        // pointless but fairly effective.
-        
-        if (resizeType & RESIZE_TYPE.L) {
-            wdim xSize = xDrag - cx;
-            if (xSize < mw) xSize = mw;	// clamp
-            x += w - xSize;
-            setWidth (xSize, 1);
-        }
-        else if (resizeType & RESIZE_TYPE.R) {
-            setWidth (xDrag + cx, -1);
-        }
-        if (resizeType & RESIZE_TYPE.T) {
-            wdim ySize = yDrag - cy;
-            if (ySize < mh) ySize = mh;
-            y += h - ySize;
-            setHeight (ySize, 1);
-        }
-        else if (resizeType & RESIZE_TYPE.B) {
-            setHeight (yDrag + cy, -1);
-        }
-        
-        // Moves lower (x,y) coordinate if necessary and repositions any sub-widgets moved by the
-        // resizing:
-        setPosition (x, y);
-    }
-    bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
-        if (b == 1 && state == false) {
-            // The mouse shouldn't have moved since the motion callback
-            // was last called, so there's nothing else to do now.
-            gui_.removeCallbacks (cast(void*) this);
-            
-            return true;    // we've handled the up-click
-        }
-        return false;       // we haven't handled it
-    }
-    wdim xDrag, yDrag;              // where a drag starts relative to x and y
-    IRenderer.RESIZE_TYPE resizeType;   // Type of current resize
-    //END Window moving and resizing
-    
-    // Load/save data:
-    public char[] name;             // The window's name (id from config file)
-    //bool edited = false;            // True if any widgets have been edited (excluding scaling)
-    
-    IGui gui_;                      // The gui managing this window
-    IRenderer rend;                 // The window's renderer
-    
-    IWidget widget;                 // The primary widget in this window.
-    
-    wdim x = -1, y = -1;            // Window position
-    wdsize w,h;                     // Window size (calculated from Widgets)
-    wdim xw, yh;                    // x+w, y+h (frequent use by clickEvent)
-    wdim widgetX, widgetY;          // Widget position (= window position plus BORDER_WIDTH)
-    wdim mw = -1, mh = -1;          // minimal size (negative means requires calculation)
-    
-    BorderDimensions border;        // Total border size (move plus resize)
-}
--- a/mde/gui/widget/createWidget.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/widget/createWidget.d	Thu Aug 07 11:25:27 2008 +0100
@@ -23,18 +23,41 @@
 import mde.gui.widget.layout;
 import mde.gui.widget.miscWidgets;
 import mde.gui.widget.TextWidget;
+import mde.gui.widget.Floating;
+import tango.util.log.Log : Log, Logger;
 
-/** Create a widget of type data[0] (see enum WIDGET_TYPES) under manager mgr, with initialisation
-* data [1..$]. */
-IChildWidget createWidget (IWidgetManager mgr, WidgetData data)
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.widget.createWidget");
+}
+
+/** Create a widget.
+ *
+ * Usually called by the widget manager's makeWidget function.
+ *
+ * Widget created of type data.ints[0] (see enum WIDGET_TYPES), with one of the following CTORs:
+ * ---
+ * this (IWidgetManager mgr, WidgetData data);
+ * // Called if (data.ints[0] & WIDGET_TYPES.TAKES_PARENT):
+ * this (IWidgetManager mgr, WidgetData data, IParentWidget parent);
+ * ---
+ *************************************************************************************************/
+IChildWidget createWidget (IWidgetManager mgr, WidgetData data, IParentWidget parent)
 in {
     assert (mgr !is null, "createWidget: mgr is null");
 } body {
-    if (data.ints.length < 1) throw new WidgetDataException ("No widget data");
+    if (data.ints.length < 1) {
+        logger.error ("No int data; creating a debug widget");
+        data.ints = [WIDGET_TYPE.Debug];
+    }
     int type = data.ints[0];    // type is first element of data
     
+    //pragma (msg, binarySearch ("type", WIDGETS));
     mixin (binarySearch ("type", WIDGETS)); // creates widget by type as new *Widget (mgr, data);
-    throw new WidgetDataException ("Bad widget type");
+    
+    // Not returned a new widget...
+    logger.error ("Bad widget type: {}; creating a debug widget instead.",type);
+    return new DebugWidget (mgr, data);
 }
 
 /+ for converting to a char[] name (unused)
@@ -50,27 +73,28 @@
 private:
 /// Widget types.
 enum WIDGET_TYPE : int {
-    WSIZABLE                = 0x1000,   // horizontally resizable
-    HSIZABLE                = 0x2000,   // vertically resizable
-    INTERACTIBLE            = 0x4000,   // any specific interaction
-    LAYOUT                  = 0x8000,   // is a layout widget (i.e. has sub-widgets)?
+    TAKES_PARENT            = 0x4000,   // CTOR takes reference to its parent
+    PARENT                  = 0x8000,   // widget can have children
     
     // Use widget names rather than usual capitals convention
     Unnamed                 = 0x0,      // Only for use by widgets not created with createWidget
     
     // blank: 0x1
     FixedBlank              = 0x1,
-    SizableBlank            = WSIZABLE | HSIZABLE | 0x1,
+    SizableBlank            = 0x2,
+    Debug                   = 0xF,
     
     // buttons: 0x10
-    Button                  = INTERACTIBLE | 0x10,
+    Button                  = 0x10,
     
     // content: 0x20
     Text		    = 0x21,
     Int			    = 0x22,
     
-    GridLayout              = LAYOUT | WSIZABLE | HSIZABLE | 0x4,
-    TrialContentLayout      = LAYOUT | WSIZABLE | HSIZABLE | 0x5
+    GridLayout              = PARENT | 0x100,
+    TrialContentLayout      = PARENT | 0x110,
+    
+    FloatingArea            = PARENT | 0x200,
 }
 
 //const char[][int] WIDGET_NAMES;
@@ -78,12 +102,14 @@
 // Only used for binarySearch algorithm generation; must be ordered by numerical values.
 const char[][] WIDGETS = [
         "FixedBlank",
+        "SizableBlank",
+        "Debug",
+        "Button",
         "Text",
         "Int",
-        "SizableBlank",
-        "Button",
         "GridLayout",
-        "TrialContentLayout"];
+        "TrialContentLayout",
+        "FloatingArea"];
 
 /* Generates a binary search algorithm. */
 char[] binarySearch (char[] var, char[][] consts) {
@@ -96,8 +122,12 @@
     } else {
         char[] ret;
         foreach (c; consts) {
-            ret ~= "if (" ~ var ~ " == WIDGET_TYPE." ~ c ~ ") {\n" ~
-                    "return new " ~ c ~ "Widget (mgr, data);\n" ~
+            ret ~=  "if (" ~ var ~ " == WIDGET_TYPE." ~ c ~ ") {\n" ~
+                    "   debug logger.trace (\"Creating new "~c~"Widget.\");\n" ~
+                    "   static if (WIDGET_TYPE."~c~" & WIDGET_TYPE.TAKES_PARENT)\n" ~
+                    "       return new " ~ c ~ "Widget (mgr, data, parent);\n" ~
+                    "   else\n" ~
+                    "       return new " ~ c ~ "Widget (mgr, data);\n" ~
                     "} else ";
         }
         ret = ret[0..$-6] ~ '\n';  // remove last else
--- a/mde/gui/widget/miscWidgets.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/gui/widget/miscWidgets.d	Thu Aug 07 11:25:27 2008 +0100
@@ -53,6 +53,28 @@
     }
 }
 
+/// A debug widget. Essentially as SizableBlankWidget but doesn't mind any amount of data and prints it.
+class DebugWidget : SizableWidget
+{
+    this (IWidgetManager mgr, WidgetData data) {
+        super (mgr, data);
+        
+        Stdout ("Debug widget - parameters: int: [");
+        foreach (x; data.ints)
+            Stdout (" ")(x);
+        Stdout (" ], char[]: [");
+        foreach (x; data.strings)
+            Stdout (" \"")(x)('"');
+        Stdout (" ]").newline;
+    }
+    
+    void draw () {
+        super.draw;
+        
+        mgr.renderer.drawBlank (x,y, w,h);
+    }
+}
+
 /// First interactible widget
 class ButtonWidget : FixedWidget
 {
--- a/mde/lookup/Options.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/lookup/Options.d	Thu Aug 07 11:25:27 2008 +0100
@@ -369,32 +369,42 @@
     //END Templates: impl & optionsThis
 }
 
-/* Special class to store all locally changed options, whatever the section. */
-class OptionChanges : Options {
+/** Special class to store all locally changed options, whatever the section. */
+class OptionChanges : IDataSection
+{
     //BEGIN Templates
     private {
+        alias Options.TName TName;
+        alias Options.TYPES TYPES;
         template Vars(A...) {
             static if (A.length) {
-                const char[] Vars = A[0].stringof~`[] `~TName!(A[0])~`s;` ~ Vars!(A[1..$]);
+                const char[] Vars = A[0].stringof~`[ID] `~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
+        // For addTag
         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];
-        	}
+                if ((id in `~TName!(T)~`s) is null)
+                    `~TName!(T)~`s[id] = parseTo!(`~T.stringof~`) (dt);
             }`;
             static if (A.length)
-                    const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
+                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, val; `~TName!(A[0])~`s)
+                    dlg ("`~A[0].stringof~`", id, serialize (val));
+                ` ~ writeAllMixin!(A[1..$]);
+            } else
+                const char[] writeAllMixin = ``;
+        }
+        
     }
     //END Templates
     // These store the actual values, but are never accessed directly except when initially added.
@@ -404,24 +414,20 @@
     this () {}
     
     void set(T) (ID id, T x) {
-        static assert (TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
-        
-        mixin (`alias opts`~TName!(T)~` optsVars;`);
-        mixin (`alias `~TName!(T)~`s vars;`);
+        static assert (Options.TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
         
-        T** p = id in optsVars;
-        if (p !is null) **p = x;
-        else {
-            vars ~= x;
-            optsVars[id] = &vars[$-1];
-        }
+        mixin (`alias `~TName!(T)~`s vars;`);
+        vars[id] = x;
     }
     
     //BEGIN Mergetag loading/saving code
-    // Reverse priority: only load symbols not currently existing
+    // HIGH_LOW priority: only load symbols not currently existing
     void addTag (char[] tp, ID id, char[] dt) {
         mixin (addTagMixin!(TYPES).addTagMixin);
     }
+    void writeAll (ItemDelg dlg) {
+        mixin(writeAllMixin!(TYPES));
+    }
     //END Mergetag loading/saving code
 }
 
--- a/mde/mergetag/Reader.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/mergetag/Reader.d	Thu Aug 07 11:25:27 2008 +0100
@@ -425,6 +425,7 @@
                     catch (TextException e) {
                         logger.error ("TextException while reading " ~ ErrFile ~ ":");	// following a parse error
                         logger.error (e.msg);
+                        logger.error ("Tag ignored: <"~type~"|"~tagID~"="~data~">");
                         // No throw: tag is just ignored
                     }
                     catch (Exception e) {
--- a/mde/setup/sdl.d	Tue Aug 05 11:51:51 2008 +0100
+++ b/mde/setup/sdl.d	Thu Aug 07 11:25:27 2008 +0100
@@ -13,7 +13,7 @@
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
-/** Just a temporary place to put SDL Init and Video stuff.
+/** Just a temporary place to put SDL init stuff.
 */
 module mde.setup.sdl;
 
@@ -178,7 +178,7 @@
 
 void cleanupSDL () {    // cleanup func
     closeJoysticks();
-    logger.trace ("cleanupSDL - middle");
+    debug logger.trace ("cleanupSDL - joysticks closed");
     SDL_Quit();
 }