changeset 159:b06b04c75e86

Finished last commit, rearranged code for the WidgetManager class. There is now a GUI options section. Created a third WidgetManager class called WidgetLoader to handle file loading/saving. Moved most of the code in WMScreen's draw/clickEvent/motionEvent functions to WidgetManager.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 21 May 2009 20:55:10 +0200
parents f132e599043f
children ccd01fde535e
files codeDoc/ideas.txt data/L10n/en-GB.mtt data/conf/guiDemo.mtt mde/gui/WMScreen.d mde/gui/WidgetLoader.d mde/gui/WidgetManager.d mde/gui/widget/Floating.d
diffstat 7 files changed, 421 insertions(+), 287 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/ideas.txt	Fri Apr 24 17:35:53 2009 +0100
+++ b/codeDoc/ideas.txt	Thu May 21 20:55:10 2009 +0200
@@ -13,8 +13,16 @@
 ->  decent rendering/theme system
 ->  events:
     ->	Click events: widgets only receive clickEvent for left-button press, other button events handled alternatively from WidgetManager?
-    ->	Click callbacks: replace with "drag callback" notifying widget of release position (and widget)?
-	->  possibly better for drag-and drop support
+    ->	Click/drag handling:
+	->  on click:
+	    ->	clicked widget recieves depress event
+	    ->	create drag monitor, which records parent and optionally runs parent method on move (which can find the widget/drop-zone underneath)
+	->  on release:
+	    ->	method from either dragged or drop-zone widget called with reference to the other:
+		->  perhaps function in dragged widget called, which is passed reference of widget underneath, and can call getDropZoneAncestor on this
+	->  drag/drop-like possibilities:
+	    ->	magnifier which is dragged from icon, creates a magnified window, and disappears on release
+	    ->	colour-picker which is dragged from a widget and dropped to choose the colour under it
 ->  Keyboard widget focus/selection:
     ->	a widget is highlighted
     ->	pressing an arrow key replaces widget with widget.nextInDirection (widget, direction)
@@ -66,6 +74,9 @@
     	+>  can use a static list widget for each type
     	->  clipboard is type specific; need to decide when to convert, etc.
             +>  enables better copying; e.g. from double 3.5e9 to int 4×10⁹
+  > Context menus:
+    > Context menu serves (editable?) content most directly under mouse cursor
+    > plus content higher up widget tree?
 > Non-static content manager
   > Separate managers for options, GUI symbols, data fields, passwords(?)
     > Optional saving/loading
@@ -82,5 +93,39 @@
       -> more work, recreating sub-widgets
   > dynamic lists (add/remove elts)
     > have to rebuild lists
+> Widget content?
 
 Extend content with a validator function/delegate, specific to each class, which takes the new value and returns it or a corrected version of it. Not so good to do it generally from Content, since setting a new value via usual method will re-trigger validator and callbacks (e.g. bad validator could cause infinite loop).
+
+
+
+From paper:
+Text styles:
+> use a few styles
+  > number and purpose of each hard-coded?
+    > titlebar, label, button, field, etc.
+  > renderer or something maps a font (font file, size, style) to each style
+
+GUI editor:
+> Widgets are wrapped with/replaced by drag & drop widgets:
+  > widgets can be dragged to another drop-zone widget
+  > widgets can be dragged to a bin or scrap area
+  > new widgets can be dragged from templates
+  > widgets can be copied by holding Ctrl
+  > widgets displaced by another are moved to the scrap area
+> side bar/editor controls:
+  > tree showing current widget structure
+    > widgets can be moved (by dragging), restructured, etc. from here the same as from the WYSISYG view
+  > new widget panel
+    > for creating new single widgets
+    > for creating new mini-trees from templates
+  > scrap panel
+    > widgets can be dragged here from WYSIWYG or tree views
+    > widgets can even be edited here
+    > widgets remaining on program/editor exit are lost?
+  > properties panel
+    > for setting widget options (initialisation data)
+  > profile selection (section of config file to save to)
+> for easy access to editor (power GUI dev mode)
+  > include a theme selection box on the GUI
+  > include a button to turn the editor on/off
--- a/data/L10n/en-GB.mtt	Fri Apr 24 17:35:53 2009 +0100
+++ b/data/L10n/en-GB.mtt	Thu May 21 20:55:10 2009 +0200
@@ -47,12 +47,16 @@
 <entry|screenH={0:"Screen height",1:"Vertical resolution (fullscreen mode)."}>
 <entry|windowW={0:"Window width",1:"Horizontal size (windowed mode)."}>
 <entry|windowH={0:"Window height",1:"Vertical size (windowed mode)."}>
+{GUI}
+<entry|raiseOnHover={0:"Raise on hover",1:"If true, floating windows are raised just by moving the mouse over them (instead of clicking)"}>
 {}
 <entry|={0:"All content"}>
 <entry|Font={0:"Font options"}>
 <entry|MiscOptions={0:"Miscellaneous options"}>
 <entry|Screen={0:"Video options"}>
+<entry|GUI={0:"GUI options"}>
 <entry|gui.switch={0:"Options"}>
 <entry|gui.switch.misc={0:"Miscellaneous"}>
 <entry|gui.switch.video={0:"Video"}>
 <entry|gui.switch.font={0:"Font"}>
+<entry|gui.switch.GUI={0:"Graphical User Interface"}>
--- a/data/conf/guiDemo.mtt	Fri Apr 24 17:35:53 2009 +0100
+++ b/data/conf/guiDemo.mtt	Thu May 21 20:55:10 2009 +0200
@@ -14,15 +14,16 @@
 <WidgetData|allContent={0:[0x2031],1:["","allPU"]}>
 <WidgetData|allPU={0:[0x6033,0,1],1:["allPU"]}>
 
-<EnumContent|gui.switch=["misc","video","font"]>
+<EnumContent|gui.switch=["misc","video","font","GUI"]>
 <WidgetData|options={0:[0x2031],1:["gui.switch","switchL"]}>
 <WidgetData|switchL={0:[0x4100,4,2,1],1:["switchVal","switchT"]}>
 <WidgetData|switchVal={0:[0x4100,4,1,2],1:["optName","optVal"]}>
-<WidgetData|switchT={0:[0x4210],1:["optMisc","optVideo","optFont"]}>
+<WidgetData|switchT={0:[0x4210],1:["optMisc","optVideo","optFont","optGUI"]}>
 
 <WidgetData|optMisc={0:[0x2031],1:["MiscOptions","optSec"]}>
 <WidgetData|optVideo={0:[0x2031],1:["Screen","optSec"]}>
 <WidgetData|optFont={0:[0x2031],1:["Font","optSec"]}>
+<WidgetData|optGUI={0:[0x2031],1:["GUI","optSec"]}>
 
 !{use optBox for no description, optDBox for descriptions under entries}
 <WidgetData|optSec={0:[0x4110,0],1:["optBox"]}>
--- a/mde/gui/WMScreen.d	Fri Apr 24 17:35:53 2009 +0100
+++ b/mde/gui/WMScreen.d	Thu May 21 20:55:10 2009 +0200
@@ -22,6 +22,7 @@
 module mde.gui.WMScreen;
 
 import mde.gui.WidgetManager;
+import mde.gui.WidgetLoader;
 import mde.gui.widget.Ifaces;
 import mde.gui.renderer.createRenderer;
 
@@ -46,9 +47,10 @@
  * sense to translate them and possibly drop events for some uses, such as if
  * the gui is drawn to a texture.
  * 
- * Public non IWidget* methods should be thread-safe.
+ * Public non IWidget* methods should be thread-safe, even to the same
+ * instance (by locking on a mutex).
  *****************************************************************************/
-scope class WMScreen : AWidgetManager, Screen.IDrawable {
+scope class WMScreen : AWidgetLoader, Screen.IDrawable {
     /** Construct a new widget manager.
      * 
      * Must be run after static this.
@@ -72,71 +74,27 @@
         synchronized(mutex) {
 	    debug (mdeDrawEvents)
 		logger.trace ("drawing");
-            if (child)
-                child.draw;
-	    if (childIPPW)
-                childIPPW.drawPopup;
-            drawPopup;
+            wmDrawWidgets();
 	}
     }
     
-    /** For mouse click events.
-     *
-     * Sends the event on to the relevant windows and all click callbacks. */
+    /** For mouse click events. */
     void clickEvent (ushort usx, ushort usy, ubyte b, bool state) {
         try {
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        if (child is null) return;
-        
-        wdabs cx = cast(wdabs) usx, cy = cast(wdabs) usy;
-        
-        // Callbacks have the highest priority receiving events (e.g. a button release)
-        foreach (dg; clickCallbacks)
-            if (dg (cx, cy, b, state)) return;
-        
-        // Update underMouse to get the widget clicked on
-        updateUnderMouse (cx, cy, state);
-        
-        // Disable keyboard input if on another widget:
-	if (keyFocus && keyFocus !is underMouse) {
-	    keyFocus.keyFocusLost;
-	    keyFocus = null;
-            input.setLetterCallback (null);
-	}
-        
-        // Finally, post the actual event:
-        if (b == 3 && state) {	// right click - open context menu
-            IContent contextContent = underMouse.content;
-            if (contextContent is null) return;
-            // NOTE: Creates new widgets every time; not optimal
-            popupContext = makeWidget (this, "context", contextContent);
-            popupContext.setup (0, 3);
-            positionPopup (underMouse, popupContext);
-            requestRedraw;
-        } else	// post other button presses to clickEvent
-        if (underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) {
-            // keyboard input requested
-            keyFocus = underMouse;
-            input.setLetterCallback (&underMouse.keyEvent);
-        }
+	    mutex.lock;
+	    scope(exit) mutex.unlock;
+	    wmMouseClick (cast(wdabs) usx, cast(wdabs) usy, b, state);
         } catch (Exception e) {
             logger.error ("clickEvent: exception processing event: {}", e.msg);
         }
     }
     
-    /** For mouse motion events.
-     *
-     * Sends the event on to all motion callbacks. */
+    /** For mouse motion events. */
     void motionEvent (ushort scx, ushort scy) {
         try {
             mutex.lock;
             scope(exit) mutex.unlock;
-            wdabs cx = cast(wdabs) scx, cy = cast(wdabs) scy;
-            foreach (dg; motionCallbacks)
-            	dg (cx, cy);
-            
-            updateUnderMouse (cx, cy, false);
+            wmMouseMotion (cast(wdabs) scx, cast(wdabs) scy);
         } catch (Exception e) {
             logger.error ("motionEvent: exception processing event: {}", e.msg);
         }
@@ -166,6 +124,10 @@
     }
     
 protected:
+    final override void setLetterCallback(void delegate(ushort, char[]) dlg) {
+	input.setLetterCallback (dlg);
+    }
+    
     /* Second stage of widget loading.
      * Note: sizeEvent should be called with window size before this. */
     final override void createRootWidget () {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/WidgetLoader.d	Thu May 21 20:55:10 2009 +0200
@@ -0,0 +1,257 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program is free software: you can redistribute it and/or modify it under the terms
+of the GNU General Public License as published by the Free Software Foundation, either
+version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/******************************************************************************
+ * A gui manager extension to load widgets from files.
+ *
+ * Public methods in this class should be thread safe (by locking on a mutex).
+ *****************************************************************************/
+module mde.gui.WidgetLoader;
+
+import mde.gui.WidgetManager;
+import mde.gui.WidgetDataSet;
+import mde.gui.exception;
+
+import mt = mde.file.mergetag.DataSet;
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.Writer;
+import mde.file.paths;
+
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this () {
+    logger = Log.getLogger ("mde.gui.WidgetLoader");
+}
+
+/******************************************************************************
+ * Contains the code for loading and saving an entire gui (more than one may
+ * exist), 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 AWidgetLoader : AWidgetManager
+{
+    /** 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;
+	super (file);
+    }
+    
+    /* 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
+            
+            // 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 (NoFileException) {
+            logger.error ("Unable to load GUI: no config file: "~fileName);
+            // just return: not a fatal error (so long as the game can run without a GUI!)
+        } 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)	// A design was previously loaded
+            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)) {
+            changes = new WidgetDataChanges (curData);
+            changesDS.sec[name] = changes;
+        }
+        
+        // Create the widgets:
+        createRootWidget;
+        underMouse = child;	// must be something
+    }
+    
+    /** Save changes, if any exist.
+    * 
+    * Is run when the manager is destroyed, but could be run at other times too. */
+    void save () {
+	preSave;
+	
+        mutex.lock;
+        scope(exit) mutex.unlock;
+        
+        // Make all widgets save any changed data:
+        child.saveChanges;
+        
+        if (changes.noChanges)
+            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;
+        }
+    }
+    
+protected:
+    // Called by derived classes, not thread safe for the same instance
+    //BEGIN WidgetManagement methods
+    /** Second stage of loading the widgets.
+    * 
+    * loadDesign handles the data; this method needs to:
+    * ---
+    * // 1. Create the root widget:
+    * child = makeWidget ("root");
+    * child.setup (0, 3);
+    * // 2. Set the size:
+    * child.setWidth  (child.minWidth,  1);
+    * child.setHeight (child.minHeight, 1);
+    * // 3. Set the position (necessary part of initialization):
+    * child.setPosition (0,0);
+    * ---
+    */
+    void createRootWidget();
+    
+    /** Called before saving (usually when the GUI is about to be destroyed, although not
+    *  necessarily). */
+    void preSave ();
+    //END WidgetManagement methods
+    
+    
+    // Dataset/design data:
+    final char[] fileName;
+    char[] defaultDesign;		// The design specified in the file header.
+    
+    // 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
+    scope mt.DataSet changesDS;		// changes and sections from user file (used for saving)
+    bool loadUserFile = true;		// still need to load user file for saving?
+}
--- a/mde/gui/WidgetManager.d	Fri Apr 24 17:35:53 2009 +0100
+++ b/mde/gui/WidgetManager.d	Thu May 21 20:55:10 2009 +0200
@@ -26,17 +26,11 @@
 
 import mde.gui.WidgetDataSet;
 import mde.gui.widget.Ifaces;
-import mde.gui.exception;
 
 import imde = mde.imde;
 import mde.content.Content;
 debug import mde.content.miscContent;	// Debug menu
 
-import mt = mde.file.mergetag.DataSet;
-import mde.file.mergetag.Reader;
-import mde.file.mergetag.Writer;
-import mde.file.paths;
-
 // Widgets to create:
 import mde.gui.widget.layout;
 import mde.gui.widget.miscWidgets;
@@ -46,7 +40,7 @@
 import mde.gui.widget.Floating;
 import mde.gui.widget.ParentContent;
 
-import tango.core.sync.Mutex;
+public import tango.core.sync.Mutex;
 import tango.util.log.Log : Log, Logger;
 import tango.util.container.SortedMap;
 
@@ -58,20 +52,23 @@
 /******************************************************************************
  * Contains the code for loading and saving an entire gui (more than one may
  * exist), but not the code for drawing it or handling user input.
+ *
+ * Methods in this class are only intended for use within the gui package,
+ * either by widgets (the IXXXWidget methods implementing from an interface in
+ * widgets.Ifaces.d) or by a derived class (back-end methods doing widget
+ * work). None of these methods are intended to be thread-safe when called
+ * concurrently on the same WidgetManager instance, but they should be thread-
+ * safe for calling on separate instances.
  * 
  * This abstract class exists solely for separating out some of the functionality.
  *****************************************************************************/
 abstract scope class AWidgetManager : 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;
-        
+    /** Construct a new widget manager.
+     *
+     * Params:
+     *	name = The file name of the config for this GUI (to identify multiple GUIs). */
+    protected this (char[] name) {
         clickCallbacks = new typeof(clickCallbacks);
         motionCallbacks = new typeof(motionCallbacks);
         
@@ -79,193 +76,12 @@
         assert (p, "MiscOptions.l10n not created!");
         p.addCallback (&reloadStrings);
         debug {	// add a debug-mode menu
-            auto lWS = new EventContent ("menus.debug."~file~".logWidgetSize");
+            auto lWS = new EventContent ("menus.debug."~name~".logWidgetSize");
             lWS.addCallback (&logWidgetSize);
         }
     }
     
-    /* 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
-            
-            // 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 (NoFileException) {
-            logger.error ("Unable to load GUI: no config file: "~fileName);
-            // just return: not a fatal error (so long as the game can run without a GUI!)
-        } 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)	// A design was previously loaded
-            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)) {
-            changes = new WidgetDataChanges (curData);
-            changesDS.sec[name] = changes;
-        }
-        
-        // Create the widgets:
-        createRootWidget;
-        underMouse = child;	// must be something
-    }
-    
-    /** Save changes, if any exist.
-    * 
-    * Is run when the manager is destroyed, but could be run at other times too. */
-    void save () {
-	preSave;
-	
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        
-        // Make all widgets save any changed data:
-        child.saveChanges;
-        
-        if (changes.noChanges)
-            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;
-        }
-    }
-    
-    /** A change callback on MiscOptions.l10n content to update widgets.
-     *
-     * Relies on another callback reloading translations to content first! */
-    protected void reloadStrings (Content) {
-        synchronized(mutex) {
-            if (child is null) return;
-            child.setup (++setupN, 2);
-            child.setWidth  (w, -1);
-            child.setHeight (h, -1);
-            child.setPosition (0,0);
-            requestRedraw;
-        }
-    }
-    
-    // These methods are only intended for use within the gui package.
-    // They are not necessarily thread-safe:
-    
+public:
     //BEGIN IParentWidget methods
     // If call reaches the widget manager there isn't any recursion.
     //NOTE: should be override
@@ -471,6 +287,79 @@
     }
     
 protected:
+    // These methods are called by derived classes to do the widget-management work
+    //BEGIN WidgetManagement methods
+    /** Draw all widgets */
+    void wmDrawWidgets() {
+	if (child)
+	    child.draw;
+	if (childIPPW)
+	    childIPPW.drawPopup;
+	drawPopup;
+    }
+    
+    /** For mouse click events.
+     *
+     * Sends the event on to the relevant windows and all click callbacks. */
+    void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) {
+	if (child is null) return;
+	
+	// Callbacks have the highest priority receiving events (e.g. a button release)
+	foreach (dg; clickCallbacks)
+	    if (dg (cx, cy, b, state)) return;
+	    
+	    // Update underMouse to get the widget clicked on
+	    updateUnderMouse (cx, cy, state);
+	
+	// Disable keyboard input if on another widget:
+	if (keyFocus && keyFocus !is underMouse) {
+	    keyFocus.keyFocusLost;
+	    keyFocus = null;
+	    setLetterCallback (null);
+	}
+	
+	// Finally, post the actual event:
+	if (b == 3 && state) {	// right click - open context menu
+	    IContent contextContent = underMouse.content;
+	    if (contextContent is null) return;
+	    // NOTE: Creates new widgets every time; not optimal
+	    popupContext = makeWidget (this, "context", contextContent);
+	    popupContext.setup (0, 3);
+	    positionPopup (underMouse, popupContext);
+	    requestRedraw;
+	} else	// post other button presses to clickEvent
+	    if (underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) {
+		// keyboard input requested
+		keyFocus = underMouse;
+		setLetterCallback (&underMouse.keyEvent);
+	    }
+    }
+    
+    /** For mouse motion events.
+     *
+     * Lock on mutex before calling. Pass new mouse coordinates. */
+    void wmMouseMotion (wdabs cx, wdabs cy) {
+	foreach (dg; motionCallbacks)
+	    dg (cx, cy);
+	
+	updateUnderMouse (cx, cy, false);
+    }
+    
+    
+    /** A change callback on MiscOptions.l10n content to update widgets.
+     *
+     * Relies on another callback reloading translations to content first! */
+    void reloadStrings (Content) {
+        synchronized(mutex) {
+            if (child is null) return;
+            child.setup (++setupN, 2);
+            child.setWidth  (w, -1);
+            child.setHeight (h, -1);
+            child.setPosition (0,0);
+            requestRedraw;
+        }
+    }
+    // for internal use
     void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) {
         auto oUM = underMouse;
         underMouse = getPopupWidget (cx, cy, closePopup);
@@ -485,25 +374,9 @@
         }
     }
     
-    /** Second stage of loading the widgets.
-    * 
-    * loadDesign handles the data; this method needs to:
-    * ---
-    * // 1. Create the root widget:
-    * child = makeWidget ("root");
-    * child.setup (0, 3);
-    * // 2. Set the size:
-    * child.setWidth  (child.minWidth,  1);
-    * child.setHeight (child.minHeight, 1);
-    * // 3. Set the position (necessary part of initialization):
-    * child.setPosition (0,0);
-    * ---
-    */
-    void createRootWidget();
-    
-    /** Called before saving (usually when the GUI is about to be destroyed, although not
-    *  necessarily). */
-    void preSave ();
+    /// This should be overloaded to set a callback receiving keyboard input.
+    abstract void setLetterCallback(void delegate(ushort, char[]));
+    //END WidgetManagement methods
     
 public:
     //BEGIN makeWidget metacode
@@ -613,19 +486,10 @@
     //END makeWidget metacode
     
 protected:
-    // Dataset/design data:
-    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?
     
+    char[] rendName;			// Name of renderer; for saving and creating renderers
     IRenderer rend;
     
     // Widgets:
--- a/mde/gui/widget/Floating.d	Fri Apr 24 17:35:53 2009 +0100
+++ b/mde/gui/widget/Floating.d	Thu May 21 20:55:10 2009 +0200
@@ -19,6 +19,7 @@
 import mde.gui.widget.AParentWidget;
 import mde.gui.exception;
 import mde.content.IContent;
+import mde.content.AStringContent;
 
 import tango.util.log.Log : Log, Logger;
 
@@ -39,7 +40,7 @@
 class FloatingAreaWidget : AParentWidget
 {
     static this () {
-	raiseOnHover = ;
+	raiseOnHover = new BoolContent ("GUI.raiseOnHover");
     }
     
     this (IWidgetManager mgr, IParentWidget parent, widgetID id, WidgetData data, IContent content) {
@@ -173,7 +174,7 @@
         debug scope (failure)
             logger.warn ("getWidget: failure; values: click; pos; width: {},{}; {},{}; {},{}", cx, cy, x, y, w, h);
         
-	size_t event = getFloatingWidget (cx,cy, raiseOnHover);
+	size_t event = getFloatingWidget (cx,cy, raiseOnHover());
 	if (event > sWData.length)
 	    return this;	// no match