changeset 113:9824bee909fd

Popup menu; works for simple menus except that clicking an item doesn't close it. Revised popup support a bit; EnumContentWidget is broken and due to be replaced.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 19 Dec 2008 10:32:28 +0000
parents fe061009029d
children b16a534f5302
files codeDoc/jobs.txt codeDoc/todo.txt data/L10n/en-GB.mtt data/conf/gui.mtt mde/content/Items.d mde/events.d mde/gui/WidgetManager.d mde/gui/widget/Ifaces.d mde/gui/widget/Popup.d mde/gui/widget/PopupMenu.d mde/gui/widget/Widget.d mde/gui/widget/createWidget.d mde/gui/widget/layout.d mde/gui/widget/miscContent.d mde/imde.d
diffstat 15 files changed, 331 insertions(+), 121 deletions(-) [+]
line wrap: on
line diff
--- a/codeDoc/jobs.txt	Sat Dec 13 12:54:43 2008 +0000
+++ b/codeDoc/jobs.txt	Fri Dec 19 10:32:28 2008 +0000
@@ -8,11 +8,15 @@
 
 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   Close popup menu on button activation/click.
+4   When child widgets are resized: must tell parent (bug);
 3   Synchronization of IContent with gui (e.g. multiple edit widgets): worth adding (temporary) callbacks?
 3   Widget saving: how to deal with modifier functions, esp. when they discard parameters? Remove feature except for dimdata and handle gui editing separately?
 3   Use of dtors - don't rely on them? Or what happens when init throws during creation - relying on undefined behaviour.
 3   glBindTexture not working with non-0 index - perhaps use a higher level graphics library at some point.
 3   Windows building/compatibility (currently partial) - tango/sys/win32/SpecialPath.d
+2   clickEvent: don't evaluate b directly but pass to a WidgetManager function to allow button configuration. Alternately, evaluate in WidgetManager's clickEvent and pass flags on to IChildWidget's clickEvent.
+2   Reduce variable passing (e.g. for some Content widgets data and id aren't even relevant). Wait for gui editor.
 2   Possibility: Could have a content method "update (T*)" to add a synchronized variable. Or could store both a content and a syncronized variable for each option within Options.
 2   Add callback to miscOpts.L10n reverting the option to it's last value if no file is found and reloading translations.
 2   Remove ability to scan, then load, mergetag sections. Not so necessary with section creator callback and allows "sliding window" type partial buffering. Also remove dataset and force use of section creator callback?
--- a/codeDoc/todo.txt	Sat Dec 13 12:54:43 2008 +0000
+++ b/codeDoc/todo.txt	Fri Dec 19 10:32:28 2008 +0000
@@ -6,6 +6,14 @@
 ->  Widgets:
 ->  scripted widgets
 ->  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
+
+EnumContent:
+->	Sub-content for each possibility of type EventContent, with callback to update EnumContent.
+->	"Tab" widget with corresponding EnumContent to control which is active.
 
 
 Scratchpad area for ideas:
--- a/data/L10n/en-GB.mtt	Sat Dec 13 12:54:43 2008 +0000
+++ b/data/L10n/en-GB.mtt	Fri Dec 19 10:32:28 2008 +0000
@@ -1,6 +1,7 @@
 {MT01}
 !{en-GB British English}
 {imde}
+<entry|menu={0:"MDE",1:"MDE interface menu"}>
 <entry|quit={0:"Quit"}>
 {Options}
 <entry|Options={0:"Options"}>
--- a/data/conf/gui.mtt	Sat Dec 13 12:54:43 2008 +0000
+++ b/data/conf/gui.mtt	Fri Dec 19 10:32:28 2008 +0000
@@ -2,10 +2,9 @@
 <char[]|Renderer="Simple">
 <char[]|Design="Working">
 {Working}
-<WidgetData|root={0:[0xC100,0,2,1],1:["bar","opts"]}>
-<WidgetData|bar={0:[0xC100,0,1,3],1:["menu","blank","quit"]}>
-<WidgetData|menu={0:[0xC011],1:["quit","Menu"]}>
-<WidgetData|quit={0:[0x2031,0x4043],1:["imde.quit"]}>
+<WidgetData|root={0:[0xC100,0,3,1],1:["bar","opts","bar"]}>
+<WidgetData|bar={0:[0xC100,0,1,3],1:["menu","blank","menu"]}>
+<WidgetData|menu={0:[0x2031,0xC011,0],1:["imde.menu"]}>
 <WidgetData|blank={0:[0x2]}>
 <WidgetData|opts={0:[0x2031,0xC100,4,2,1],1:["Options","optName","optSecs"]}>
 <WidgetData|optSecs={0:[0x6030,4],1:["optSec"]}>
--- a/mde/content/Items.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/content/Items.d	Fri Dec 19 10:32:28 2008 +0000
@@ -21,7 +21,7 @@
 import mde.content.Content;
 import mde.gui.exception;
 
-import mde.imde;
+import imde = mde.imde;
 import mde.lookup.Options;
 import mde.lookup.Translation;
 
@@ -59,8 +59,10 @@
 	    }
 	} else if (h == "imde") {
 	    h = head (item);
-	    if (h == "quit" && item is null)
-		return quit;
+	    if (h == "menu" && item is null)
+		return imde.menu;
+	    else if (h == "quit" && item is null)
+		return imde.quit;
 	}
 	throw new ContentItemException (h);
     }
@@ -109,8 +111,10 @@
 	}
 	
 	// Translate imde:
+	trle = Translation.get ("imde").getStruct ("menu");
+	imde.menu.name (trle.name, trle.desc);
 	trle = Translation.get ("imde").getStruct ("quit");
-	quit.name (trle.name, trle.desc);
+	imde.quit.name (trle.name, trle.desc);
 	
 	currentL10n = miscOpts.L10n();
     }
--- a/mde/events.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/events.d	Fri Dec 19 10:32:28 2008 +0000
@@ -21,14 +21,11 @@
 
 import imde = mde.imde;
 import mde.setup.Screen;
-
 import mde.input.Input;
 
 import derelict.sdl.events;
-
 import tango.time.Time;
 import tango.util.log.Log : Log, Logger;
-import tango.io.Stdout;
 
 private Logger logger;
 static this() {
@@ -54,7 +51,7 @@
             default:
                 try {
                     if (!imde.input (event))
-                        debug Stdout ("Unrecognised event with code ")(event.type).newline;
+                        logger.warn ("Unrecognised event with code {}", event.type);
                 } catch (Exception e) {
                     logger.error ("Caught input exception; event will be ignored. Exception was:");
                     logger.error (e.msg);
--- a/mde/gui/WidgetManager.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/WidgetManager.d	Fri Dec 19 10:32:28 2008 +0000
@@ -35,6 +35,7 @@
 import tango.core.sync.Mutex;
 import tango.util.log.Log : Log, Logger;
 import tango.util.container.CircularList;	// pop-up draw callbacks
+import tango.util.container.SortedMap;
 
 private Logger logger;
 static this () {
@@ -62,6 +63,8 @@
         super(file);
         
         Screen.addDrawable (this);
+	clickCallbacks = new typeof(clickCallbacks);
+	motionCallbacks = new typeof(motionCallbacks);
     }
     
     // this() runs during static this(), when imde.input doesn't exist. init() runs later.
@@ -99,11 +102,12 @@
         
         // 1. Callbacks have the highest priority recieving events (e.g. a button release)
         foreach (dg; clickCallbacks)
-            // See IWidgetManager.addClickCallback's documentation:
             if (dg (cast(wdabs)cx, cast(wdabs)cy, b, state)) return;
         
         // 2. Then pop-ups
-        IChildWidget widg;
+	static IChildWidget[] removedPopupParents;
+	uint removedPopups = 0;
+        IChildWidget widg;	// widget clicked on
         {
             auto i = popups.iterator;
             foreach (popup; i) with (popup) {
@@ -111,6 +115,9 @@
                     cy < y || cy >= y + h) {
                     i.remove;
                     requestRedraw;
+		    if (removedPopupParents.length <= removedPopups)
+			removedPopupParents.length = removedPopupParents.length * 2 + 4;
+		    removedPopupParents[removedPopups++] = parent;
                 } else {
                     widg = widget.getWidget (cast(wdabs)cx,cast(wdabs)cy);
                     break;
@@ -133,19 +140,40 @@
 		imde.input.setLetterCallback (&widg.keyEvent);
 	    }
 	}
+	
+	// Tell parents their popups closed (needs to be after clickEvent for PopupMenuWidget)
+	while (removedPopups)
+	    removedPopupParents[--removedPopups].popupRemoved;
     }
     
     /** For mouse motion events.
      *
      * Sends the event on to all motion callbacks. */
-    void motionEvent (ushort cx, ushort cy) {
+    void motionEvent (ushort scx, ushort scy) {
         debug scope (failure)
                 logger.warn ("motionEvent: failed!");
         mutex.lock;
         scope(exit) mutex.unlock;
-        
+	wdabs cx = cast(wdabs) scx, cy = cast(wdabs) scy;
         foreach (dg; motionCallbacks)
-            dg (cast(wdabs)cx, cast(wdabs)cy);
+            dg (cx, cy);
+	
+	IChildWidget ohighlighted = highlighted;
+	foreach (popup; popups) with (popup) {
+	    if (cx >= x && cx < x+w && cy >= y && cy < y+h) {
+		highlighted = widget.getWidget (cx,cy);
+		goto foundPopup;
+	    }
+	}
+	highlighted = null;	// not over a popup
+	foundPopup:
+	if (ohighlighted != highlighted) {
+	    if (ohighlighted)
+		ohighlighted.highlight (false);
+	    if (highlighted)
+		highlighted.highlight (true);
+	    requestRedraw;
+	}
     }
     
     
@@ -173,32 +201,38 @@
         return rend;
     }
     
-    /** Place a pop-up widget near px,py.
+    /** Place a pop-up widget (widg) above or below parent.
      *
      * WidgetManager sets its position, draws it, passes it click events and removes it; other
-     * functionality should be handled by the widget's parent. */
-    void addPopup (wdabs px, wdabs py, IChildWidget widg) {
-        ActivePopup popup;
-        with (popup) {
-            widget = widg;
-            w = widg.width;
-            h = widg.height;
-            x = px + w > this.w ? this.w - w : px;
-            if (x < 0) x = 0;
-            y = py + h > this.h ? this.h - h : py;
-            if (y < 0) y = 0;
-            widget.setPosition (x, y);
-        }
-        popups.prepend (popup);
+     * functionality should be handled by the widget's parent.
+     *
+     * Popups currently should not change their size while active. */
+    void addPopup (IChildWidget parent, IChildWidget widg) {
+	debug assert (parent && widg, "addPopup: null widget");
+	ActivePopup popup;
+	popup.parent = parent;
+	with (popup) {
+	    widget = widg;
+	    w = widg.width;
+	    h = widg.height;
+	    x = parent.xPos;				// align on left edge
+	    if (x+w > this.w) x += parent.width - w;	// align on right edge
+	    y = parent.yPos + parent.height;		// place below
+	    if (y+h > this.h) y = parent.yPos - h;	// place above
+	    widget.setPosition (x, y);
+	}
+	popups.prepend (popup);
+	requestRedraw;
     }
-    void removePopup (IChildWidget widg) {	//FIXME: not optimal (maybe change popups though?)
+    /+ Not required but possibly useful later. Not optimal.
+    void removePopup (IChildWidget widg) {
 	auto i = popups.iterator;
 	foreach (popup; i) {
 	    if (popup.widget is widg)
 		i.remove;
 	}
 	requestRedraw;
-    }
+    }+/
 
     void requestRedraw () {
         imde.mainSchedule.request(imde.SCHEDULE.DRAW);
@@ -211,8 +245,8 @@
         motionCallbacks[dg.ptr] = dg;
     }
     void removeCallbacks (void* frame) {
-        clickCallbacks.remove(frame);
-        motionCallbacks.remove(frame);
+        clickCallbacks.removeKey(frame);
+        motionCallbacks.removeKey(frame);
     }
     //END IWidgetManager methods
     
@@ -250,15 +284,17 @@
 private:
     struct ActivePopup {
         IChildWidget widget;
+	IChildWidget parent;
         wdabs x,y;
         wdsize w,h;
     }
     IRenderer rend;
     CircularList!(ActivePopup) popups;// Pop-up [menus] to draw. First element is top popup.
-    // callbacks indexed by their frame pointers:
-    bool delegate(wdabs cx, wdabs cy, ubyte b, bool state) [void*] clickCallbacks;
-    void delegate(wdabs cx, wdabs cy) [void*] motionCallbacks;
+    // callbacks indexed by their frame pointers. Must support removal of elements in foreach:
+    SortedMap!(void*,bool delegate(wdabs cx, wdabs cy, ubyte b, bool state)) clickCallbacks;
+    SortedMap!(void*,void delegate(wdabs cx, wdabs cy)) motionCallbacks;
     IChildWidget keyFocus;	// widget receiving keyboard input when non-null
+    IChildWidget highlighted;	// NOTE: in some ways should be same as keyFocus
 }
 
 
--- a/mde/gui/widget/Ifaces.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/widget/Ifaces.d	Fri Dec 19 10:32:28 2008 +0000
@@ -89,9 +89,12 @@
     * provides the possibility of per-window renderers (if desired). */
     IRenderer renderer ();
     
-    /** Add/remove a pop-up [menu] to be drawn. */
-    void addPopup (wdabs x, wdabs y, IChildWidget popup);
-    void removePopup (IChildWidget popup);	/// ditto
+    /** Add/remove a pop-up widget to be drawn.
+     *
+     * Place popup as close to x,y as possible, or
+     * place popup next to parent (usually below). */
+    void addPopup (IChildWidget parent, IChildWidget popup);
+    //void removePopup (IChildWidget popup);	Possibly useful later.
     
     // User input:
     /** Add a mouse click callback.
@@ -100,10 +103,7 @@
      * simply all click events on the widget (as clickEvent recieves).
      *
      * The delegate should return true if it accepts the event and no further processing is
-     * required (i.e. the event should not be handled by anything else), false otherwise.
-     * 
-     * Note that this is not a mechanism to prevent unwanted event handling, and in the future
-     * may be removed (so event handling cannot be cut short). */
+     * required (i.e. the event should not be handled by anything else), false otherwise. */
     void addClickCallback (bool delegate (wdabs cx, wdabs cy, ubyte b, bool state) dg);
     
     /** Add a mouse motion callback: delegate will be called for all motion events recieved by the
@@ -205,6 +205,10 @@
     wdim width ();
     wdim height();      /// ditto
     
+    /** (Smallest) coordinates of widget. */
+    wdabs xPos ();
+    wdabs yPos ();	/// ditto
+    
     /** Used to adjust the size.
      *
      * Params:
@@ -236,7 +240,7 @@
      * (x,y).
      *
      * Note: use global coordinates (x,y) not coordinates relative to the widget. */
-    IChildWidget getWidget (wdim x, wdim y);
+    IChildWidget getWidget (wdabs x, wdabs y);
     
     /** Receive a mouse click event at cx,cy from button b (1-5 correspond to L,M,B, wheel up,down)
      * which is a down-click if state is true.
@@ -255,6 +259,14 @@
     
     /** Called when keyboard input focus is lost. */
     void keyFocusLost ();
+    
+    /** Called when the mouse moves over the button and when it leaves. No need to call
+     * requestRedraw. */
+    void highlight (bool state);
+    
+    /** After adding a pop-up widget with mgr.addPopup(this,popupWidget), when that pop-up closes
+     * the manager calls requestRedraw, clickEvent if applicable, and then this function. */
+    void popupRemoved ();
 //END Events
     
     /** Draw, using the stored values of x and y.
--- a/mde/gui/widget/Popup.d	Sat Dec 13 12:54:43 2008 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +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/>. */
-
-/// Pop-up widgets.
-module mde.gui.widget.Popup;
-
-import mde.gui.widget.Widget;
-import mde.content.IContent;
-
-/** Shows a "pop-up" widget tree when clicked. */
-class PopupButtonWidget : AButtonWidget
-{
-    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
-	WDCheck (data, 1,2);
-	subWidget = mgr.makeWidget (data.strings[0], content);
-        
-        adapter = mgr.renderer.getAdapter;
-	adapter.text = data.strings[1];
-        adapter.getDimensions (mw, mh);
-        w = mw;
-        h = mh;
-        super (mgr, id, data);
-    }
-    
-    bool setup (uint n, uint flags) {
-	return subWidget.setup (n,flags);
-    }
-    
-    bool saveChanges () {
-	return subWidget.saveChanges;
-    }
-    
-    void activated () {
-        mgr.addPopup (x,y, subWidget);
-    }
-    
-    void draw () {
-        super.draw ();
-        adapter.draw (x,y);
-    }
-    
-protected:
-    IChildWidget subWidget;
-    
-    IRenderer.TextAdapter adapter;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/gui/widget/PopupMenu.d	Fri Dec 19 10:32:28 2008 +0000
@@ -0,0 +1,168 @@
+/* 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/>. */
+
+/*************************************************************************************************
+ * Pop-up menus based on content structures.
+ *************************************************************************************************/
+module mde.gui.widget.PopupMenu;
+
+import mde.gui.widget.Widget;
+import mde.gui.widget.textContent;
+import mde.gui.widget.TextWidget;
+import mde.gui.widget.layout;
+
+import mde.content.Content;
+import mde.gui.exception;
+
+debug {
+    import tango.util.log.Log : Log, Logger;
+    private Logger logger;
+    static this () {
+	logger = Log.getLogger ("mde.gui.widget.PopupMenu");
+    }
+}
+
+/*************************************************************************************************
+ * Widget which pops up a menu based on a content.
+ *************************************************************************************************/
+class PopupMenuWidget : AParentSingleWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
+	content = c;
+	WDCMinCheck (data, 1,0, content);
+	subWidget = menuContent (mgr, id, data, content);
+	
+	adapter = mgr.renderer.getAdapter;
+	adapter.text = content.toString (1);
+	adapter.getDimensions (mw, mh);
+	w = mw;
+	h = mh;
+	super (mgr, id, data);
+    }
+    
+    int clickEvent (wdabs, wdabs, ubyte b, bool state) {
+	if (b == 1 && state == true) {
+	    // If active, the popup is closed by WidgetManager since the click isn't on the popup.
+	    if (!pushed) {
+		pushed = true;
+		mgr.addPopup (this, subWidget);	// causes redraw
+		mgr.addClickCallback (&openMenuCallback);	// prevents first up-click from closing menu, if on self.
+	    }
+	}
+	return 0;
+    }
+    
+    void popupRemoved () {
+	pushed = false;
+    }
+    
+    void draw () {
+	mgr.renderer.drawButton (x,y, w,h, pushed);
+	adapter.draw (x,y);
+    }
+    
+protected:
+    bool openMenuCallback (wdabs cx, wdabs cy, ubyte b, bool state) {
+	if (b == 1 && state == false) {	// receive first up-click
+	    mgr.removeCallbacks (cast(void*) this);
+	    if (cx >= x && cx < x+w && cy >= y && cy < y+h)
+		return true;		// up-click is on self; don't close the menu
+	}
+	return false;
+    }
+    bool pushed = false;
+    IRenderer.TextAdapter adapter;
+    IContent content;
+}
+
+/*************************************************************************************************
+ * A function which returns the most appropriate content menu widget.
+ *************************************************************************************************/
+IChildWidget menuContent (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) { if (c is null) throw new ContentException;
+    if (cast(ContentList) c)
+	return new MenuContentListWidget(mgr,id,data,c);
+    else if (cast(EventContent) c)
+	return new MenuButtonContentWidget(mgr,id,data,c);
+    else // generic uneditable option
+        return new DisplayContentWidget(mgr,id,data,c);
+}
+
+/** A menu content-button, like ButtonContentWidget, but which can be activated with the up-click.
+ */
+class MenuButtonContentWidget : ATextWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent c) {
+	content = cast(EventContent) c;
+	WDCMinCheck (data, 1,0, content);
+	adapter = mgr.renderer.getAdapter ();
+	super (mgr, id, data);
+    }
+    
+    bool setup (uint n, uint flags) {
+	if (!(flags & 3)) return false;	// string or renderer (and possibly font) changed
+	adapter.text = content.toString(1);
+	return super.setup (n, 3);	// force redimensioning
+    }
+    
+    int clickEvent (wdabs, wdabs, ubyte b, bool state) {
+	if (b == 1) {	// on up or down click
+	    pushed = false;
+	    mgr.requestRedraw;
+	    content.endEvent;
+	}
+	return 0;
+    }
+    
+    void highlight (bool state) {
+	pushed = state;
+    }
+    
+    void draw () {
+	mgr.renderer.drawButton (x,y, w,h, pushed);
+	adapter.draw (x,y);
+    }
+    
+protected:
+    EventContent content;
+    bool pushed;
+}
+
+/// Similar to layout.ContentListWidget but creates sub-widgets with menuContent.
+class MenuContentListWidget : GridWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
+	cList = cast(ContentList) content;
+	WDCCheck (data, 2, 0, cList);
+	
+	cols = 1;
+	if ((rows = cList.list.length) > 0) {
+	    subWidgets.length = rows;
+	    foreach (i, c; cList.list) {
+		subWidgets[i] = menuContent (mgr,id,data,c);
+	    }
+	} else {
+	    rows = 1;
+	    subWidgets = [mgr.makeWidget (id, new ErrorContent ("<empty list>"))];
+	}
+	super (mgr, id, data);
+    }
+    
+    bool saveChanges () {
+	return false;	// sub-widgets don't have an id
+    }
+    
+private:
+    ContentList cList;
+}
--- a/mde/gui/widget/Widget.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/widget/Widget.d	Fri Dec 19 10:32:28 2008 +0000
@@ -84,6 +84,13 @@
         return h;
     }
     
+    wdabs xPos () {
+	return x;
+    }
+    wdabs yPos () {
+	return y;
+    }
+    
     deprecated void getCurrentSize (out wdim cw, out wdim ch) {
         cw = w;
         ch = h;
@@ -122,6 +129,12 @@
     /* Dummy functions: suitable for widgets with no text input. */
     void keyEvent (ushort, char[]) {}
     void keyFocusLost () {}
+    
+    // Currently only applies to popup widgets.
+    void highlight (bool state) {}
+    
+    // Only useful to widgets creating popups.
+    void popupRemoved () {}
 //END Events
     
     /* Basic draw method: draw the background (all widgets should do this). */
@@ -176,7 +189,7 @@
 }
 
 /*************************************************************************************************
- * An abstract base widget class for parent widgets (many parent widgets don't use these methods).
+ * Abstract base widget classes to facilitate parent widgets.
  * 
  * Parent widgets probably need to overload these functions (from AWidget):
  * setup, saveChanges, setPosition, getWidget, draw, setWidth and setHeight.
@@ -189,8 +202,10 @@
     
     bool setup (uint n, uint flags) {
 	bool c = false;
-	foreach (w; subWidgets)
+	foreach (w; subWidgets) {
+	    debug assert (w);
 	    c |= w.setup (n,flags);
+	}
 	return c;
     }
     
@@ -204,6 +219,25 @@
 protected:
     IChildWidget[] subWidgets;
 }
+/** ditto */
+abstract class AParentSingleWidget : AWidget
+{
+    this (IWidgetManager mgr, widgetID id, WidgetData data) {
+	super (mgr, id, data);
+    }
+    
+    bool setup (uint n, uint flags) {
+	debug assert (subWidget);
+	return subWidget.setup (n,flags);
+    }
+    
+    bool saveChanges () {
+	return subWidget.saveChanges;
+    }
+	
+protected:
+    IChildWidget subWidget;
+}
 
 /** A base for fixed-size widgets taking their size from the creation data. */
 class FixedWidget : AWidget {
@@ -286,5 +320,3 @@
 protected:
     bool pushed = false;        /// True if button is pushed in (visually)
 }
-
-
--- a/mde/gui/widget/createWidget.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/widget/createWidget.d	Fri Dec 19 10:32:28 2008 +0000
@@ -30,7 +30,7 @@
 import mde.gui.widget.miscContent;
 import mde.gui.widget.textContent;
 import mde.gui.widget.Floating;
-import mde.gui.widget.Popup;
+import mde.gui.widget.PopupMenu;
 import tango.util.log.Log : Log, Logger;
 
 private Logger logger;
@@ -98,7 +98,7 @@
     
     // buttons: 0x10
     Button		= 0x10,
-    PopupButton		= TAKES_CONTENT | PARENT | 0x11,
+    PopupMenu		= TAKES_CONTENT | PARENT | 0x11,
     
     // labels: 0x20
     ContentLabel	= TAKES_CONTENT | 0x20,
@@ -113,7 +113,7 @@
     BoolContent		= TAKES_CONTENT | 0x41,
     AStringContent	= TAKES_CONTENT | 0x42,
     ButtonContent	= TAKES_CONTENT | 0x43,
-    EnumContent	= TAKES_CONTENT | 0x44,
+    //EnumContent	= TAKES_CONTENT | 0x44,
     
     GridLayout		= TAKES_CONTENT | PARENT | 0x100,
     ContentList		= TAKES_CONTENT | PARENT | 0x110,
@@ -136,10 +136,10 @@
         "BoolContent",
 	"AStringContent",
 	"ButtonContent",
-	"EnumContent",
+	//"EnumContent",
         "editContent",
         "FloatingArea",
-	"PopupButton",
+	"PopupMenu",
         "GridLayout",
 	"ContentList"];
 
--- a/mde/gui/widget/layout.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/widget/layout.d	Fri Dec 19 10:32:28 2008 +0000
@@ -99,8 +99,6 @@
 class ContentListWidget : GridWidget
 {
     this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) {
-        debug scope (failure)
-                logger.warn ("TrialContentLayoutWidget: failure");
 	cList = cast(ContentList) content;
 	WDCCheck (data, 2, 1, cList);
 	
@@ -235,7 +233,7 @@
     // Find the relevant widget.
     IChildWidget getWidget (wdim cx, wdim cy) {
         debug scope (failure)
-            logger.warn ("getWidget: failure; values: click, pos, width - {}, {}, {} - {}, {}, {}", cx, x, w, cy, y, h);
+            logger.warn ("getWidget: failure; values: click; pos; width: {},{}; {},{}; {},{}", cx, cy, x, y, w, h);
         debug assert (cx >= x && cx < x + w && cy >= y && cy < y + h, "getWidget: not on widget (code error)");
         
         // Find row/column:
@@ -291,8 +289,10 @@
 	if (sADD_n == n) return;	// cached data is current
 	sADD_n = n;
 	
-	foreach (widg; subWidgets)	// make sure all subwidgets have been set up
+	foreach (widg; subWidgets) {	// make sure all subwidgets have been set up
+	    debug assert (widg);
 	    widg.setup (n,flags);
+	}
 	// make sure both AlignColumns are set up (since first call to setup(n) calls reset):
 	col.setup (n, flags);
 	row.setup (n, flags);
--- a/mde/gui/widget/miscContent.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/gui/widget/miscContent.d	Fri Dec 19 10:32:28 2008 +0000
@@ -67,7 +67,9 @@
 	    return new AStringContentWidget(mgr,id,data,c);
     } else if (cast(ContentList) c)
 	return new ContentListWidget(mgr,id,data,c);
-    else        // generic uneditable option
+    else if (cast(EventContent) c)
+	return new ButtonContentWidget(mgr,id,data,c);
+    else	// generic uneditable option
         return new DisplayContentWidget(mgr,id,data,c);
 }
 
@@ -153,7 +155,7 @@
     }
     
     void activated () {
-	mgr.addPopup (x,y, internalWidg);
+	mgr.addPopup (this, internalWidg);
     }
     
     void draw () {
@@ -207,7 +209,7 @@
 			    break;
 		    logger.trace ("Setting value: {}", i);
 		    content = i;
-		    mgr.removePopup (this);
+		    //mgr.removePopup (this);
 		    changeContent;
 		}
 		
--- a/mde/imde.d	Sat Dec 13 12:54:43 2008 +0000
+++ b/mde/imde.d	Fri Dec 19 10:32:28 2008 +0000
@@ -29,9 +29,14 @@
     quit = (new EventContent("quit")).addCallback ((AContent){
 	run = false;
     });
+    a = new EventContent("a");
+    b = new EventContent("b");
+    menu = new ContentList ("menu",[quit,a,b]);
 }
 
+ContentList menu;	/// Root menu for imde
 EventContent quit;	/// A content triggering mde to halt
+EventContent a,b;	/// Dummy items
 
 Scheduler mainSchedule; /// The schedule used by the main loop.