diff mde/gui/WidgetManager.d @ 121:5b37d0400732

Widgets now receive and store their parent (IParentWidget). Infinite widget recursion checks. WidgetManager code redistributed. WidgetManager code redistributed between classes; WMScreen class moved to WMScreen.d. addContent function now calls makeWidget with another id.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 02 Jan 2009 18:07:10 +0000
parents aba2dd815a1f
children d3b2cefd46c9
line wrap: on
line diff
--- a/mde/gui/WidgetManager.d	Thu Jan 01 15:16:00 2009 +0000
+++ b/mde/gui/WidgetManager.d	Fri Jan 02 18:07:10 2009 +0000
@@ -14,27 +14,33 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 /*************************************************************************************************
- * The gui manager class.
+ * The gui manager class base.
  *
- * This is the module to use externally to create a graphical user interface (likely also with
- * content modules).
+ * This contains most of the code required by a window manager, but does not interact with a screen
+ * or get user input. Rendering is handled separately by the renderer anyway.
  *************************************************************************************************/
 module mde.gui.WidgetManager;
 
 import mde.gui.WidgetDataSet;
 import mde.gui.widget.Ifaces;
-import mde.gui.renderer.createRenderer;
+import mde.gui.exception;
+import mde.content.Content;
 
 import imde = mde.imde;
-import mde.input.Input;
-import mde.scheduler.Scheduler;
-import mde.setup.Screen;
-import Items = mde.content.Items;	// loadTranslation
-import mde.lookup.Options;	// miscOpts.L10n callback
+import mde.file.mergetag.Reader;
+import mde.file.mergetag.Writer;
+import mde.setup.paths;
+
+// Widgets to create:
+import mde.gui.widget.layout;
+import mde.gui.widget.miscWidgets;
+import mde.gui.widget.TextWidget;
+import mde.gui.widget.miscContent;
+import mde.gui.widget.Floating;
+import mde.gui.widget.PopupMenu;
 
 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;
@@ -43,283 +49,12 @@
 }
 
 /*************************************************************************************************
- * The widget manager.
- * 
- * This provides a layer on top of WidgetLoader, handling input and rendering. Other functionality
- * is contained in the super class, to simplify supporting new input/graphics libraries.
- * 
- * Currently mouse coordinates are passed to widgets untranslated. It may make sense to translate
- * them and possibly drop events for some uses, such as if the gui is drawn to a texture.
- * 
- * Aside from the IWidgetManager methods, this class should be thread-safe.
- *************************************************************************************************/
-class WidgetManager : WidgetLoader, Screen.IDrawable {
-    /** Construct a new widget manager.
-     * 
-     * params:
-     *  fileName = Name of file specifying the gui, excluding path and extension.
-     */
-    this (char[] file) {
-        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.
-    void init () {
-        // Doesn't need a lock - cannot conflict with other class functions.
-        // Events we want to know about:
-        imde.input.addMouseClickCallback(&clickEvent);
-        imde.input.addMouseMotionCallback(&motionEvent);
-	
-	Items.loadTranslation ();
-	miscOpts.L10n.addCallback (&reloadStrings);
-    }
-    
-    
-    /** Draw the gui. */
-    void draw() {
-        synchronized(mutex) {
-            if (child)
-                child.draw;
-	    foreach (popup; popups)
-		popup.widget.draw();
-	}
-    }
-    
-    
-    /** For mouse click events.
-     *
-     * Sends the event on to the relevant windows and all click callbacks. */
-    void clickEvent (ushort usx, ushort usy, ubyte b, bool state) {
-        debug scope (failure)
-            logger.warn ("clickEvent: failed!");
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        if (child is null) return;
-        
-        wdabs cx = cast(wdabs) usx, cy = cast(wdabs) usy;
-        
-        // 1. Callbacks have the highest priority recieving events (e.g. a button release)
-        foreach (dg; clickCallbacks)
-            if (dg (cx, cy, b, state)) return;
-        
-        // 2. Then pop-ups: close from top, depending on click pos
-        // Note: assumes each evaluated popup's parent is not under another still open popup.
-        // Also assumes popup's parent doesn't have other children in its box.
-        size_t removeTo = popups.length;
-        bool eventDone;		// don't pass clickEvent
-        IChildWidget widg;	// widget clicked on
-        foreach_reverse (i,popup; popups) with (popup) {
-            if (cx < x || cx >= x + w ||
-                cy < y || cy >= y + h) {	// on popup
-                if (parent.onSelf (cx, cy)) {
-                    if (parent.popupParentClick()) removeTo = i;
-                    eventDone = true;
-                    break;
-                } else {
-                    removeTo = i;
-                    parent.popupClose;
-                }
-            } else {
-                widg = widget.getWidget (cast(wdabs)cx,cast(wdabs)cy);
-                break;
-            }
-        }
-        if (removeTo < popups.length) {
-            requestRedraw;
-            popups = popups[0..removeTo];
-        }
-        if (eventDone)
-            return;
-        
-        // 3. Then the main widget tree
-        debug assert (cx < child.width && cy < child.height, "WidgetManager: child doesn't cover whole area (code error)");
-        if (widg is null)
-            widg = child.getWidget (cast(wdabs)cx,cast(wdabs)cy);
-	if (keyFocus && keyFocus !is widg) {
-	    keyFocus.keyFocusLost;
-	    keyFocus = null;
-	    imde.input.setLetterCallback (null);
-	}
-        if (widg !is null) {
-	    if (widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) {
-		keyFocus = widg;
-		imde.input.setLetterCallback (&widg.keyEvent);
-	    }
-	}
-    }
-    
-    /** For mouse motion events.
-     *
-     * Sends the event on to all motion callbacks. */
-    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 (cx, cy);
-	
-	IChildWidget ohighlighted = highlighted;
-	foreach_reverse (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;
-	}
-    }
-    
-    
-    void sizeEvent (int nw, int nh) {   // IDrawable function
-        mutex.lock;
-        scope(exit) mutex.unlock;
-        
-        w = cast(wdim) nw;
-        h = cast(wdim) nh;
-        
-        if (w < mw || h < mh)
-            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
-        
-        if (!child) return;     // if not created yet.
-        child.setWidth  (w, -1);
-        child.setHeight (h, -1);
-        child.setPosition (0,0);
-    }
-    
-    //BEGIN IWidgetManager methods
-    // These methods are only intended for use within the gui package. They are not necessarily
-    // thread-safe.
-    IRenderer renderer () {
-        assert (rend !is null, "WidgetManager.renderer: rend is null");
-        return rend;
-    }
-    
-    void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) {
-	debug assert (parnt && widg, "addPopup: null widget");
-        if (popups.length >= popupsMem.length)
-            popupsMem.length = popupsMem.length * 2 + 2;
-        with (popupsMem[popups.length]) {
-            parent = parnt;
-            widget = widg;
-	    w = widg.width;
-	    h = widg.height;
-            if (flags & 1) {
-	    	y = parent.yPos;
-            	if (y+h > this.h) y += parent.height - h;
-	    	x = parent.xPos + parent.width;
-	    	if (x+w > this.w) x = parent.xPos - w;
-            } else {
-                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 = popupsMem[0..popups.length+1];
-	requestRedraw;
-    }
-    void removePopup (IChildWidget parnt) {
-	foreach_reverse (i,popup; popups) {
-	    if (popup.parent is parnt)
-		popups = popups[0..i];
-	}
-	requestRedraw;
-    }
-
-    void requestRedraw () {
-        imde.mainSchedule.request(imde.SCHEDULE.DRAW);
-    }
-    
-    void addClickCallback (bool delegate(wdabs, wdabs, ubyte, bool) dg) {
-        clickCallbacks[dg.ptr] = dg;
-    }
-    void addMotionCallback (void delegate(wdabs, wdabs) dg) {
-        motionCallbacks[dg.ptr] = dg;
-    }
-    void removeCallbacks (void* frame) {
-        clickCallbacks.removeKey(frame);
-        motionCallbacks.removeKey(frame);
-    }
-    //END IWidgetManager methods
-    
-protected:
-    /* Second stage of widget loading.
-     * Note: sizeEvent should be called with window size before this. */
-    final override void createRootWidget () {
-        // The renderer needs to be created on the first load, but not after this.
-        if (rend is null)
-            rend = createRenderer (rendName);
-	popups = null;
-        
-        child = makeWidget ("root");
-        child.setup (0, 3);
-        
-        mw = child.minWidth;
-        mh = child.minHeight;
-        
-        if (w < mw || h < mh)
-            logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h);
-        
-        child.setWidth  (w, -1);
-        child.setHeight (h, -1);
-        child.setPosition (0,0);
-    }
-    
-    final override void preSave () {
-	if (keyFocus) {
-	    keyFocus.keyFocusLost;
-	    keyFocus = null;
-	    imde.input.setLetterCallback (null);
-	}
-    }
-    
-private:
-    struct ActivePopup {
-        IChildWidget widget;
-	IChildWidget parent;
-        wdabs x,y;
-        wdsize w,h;
-    }
-    IRenderer rend;
-    ActivePopup[] popups;	// Pop-up [menus] to draw. Last element is top popup.
-    ActivePopup[] popupsMem;	// allocated memory for popups
-    // 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
-}
-
-
-import mde.gui.exception;
-import mde.content.Content;	// Content passed to a callback
-import mde.gui.widget.createWidget;
-
-import mde.file.mergetag.Reader;
-import mde.file.mergetag.Writer;
-import mde.setup.paths;
-
-/*************************************************************************************************
  * 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 WidgetLoader : IWidgetManager
+abstract scope class AWidgetManager : IWidgetManager
 {
     /** Construct a new widget loader.
     * 
@@ -503,6 +238,110 @@
 	requestRedraw;
     }
     
+    //BEGIN IParentWidget methods
+    // If call reaches the widget manager there isn't any recursion.
+    //NOTE: should be override
+    final void recursionCheck (widgetID) {}
+    //END IParentWidget methods
+    
+    //BEGIN IWidgetManager methods
+    override IChildWidget makeWidget (IParentWidget parent, widgetID id, IContent content = null)
+    {
+        debug assert (parent, "makeWidget: parent is null (code error)");
+        debug scope (failure)
+                logger.warn ("Creating widget \""~id~"\" failed.");
+        
+        WidgetData data = curData[id];
+        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
+    
+        try {
+            // Statically programmed binary search on type, returning a new widget or calling a
+            // function:
+            //pragma (msg, binarySearch ("type", WIDGETS));
+            mixin (binarySearch ("type", WIDGETS));
+            // Not returned a new widget:
+            logger.error ("Bad widget type: {}; creating a debug widget instead",type);
+        } catch (Exception e) {
+            logger.error ("Error creating widget: {}; creating a debug widget instead.", e.msg);
+        }
+    
+        return new DebugWidget (this, this, id, data);
+    }
+    
+    override WidgetData widgetData (widgetID id) {
+        return curData[id];
+    }
+    override void widgetData (widgetID id, WidgetData d) {
+        changes[id] = d;		// also updates WidgetDataSet in data.
+    }
+    
+    override wdims dimData (widgetID id) {
+        return curData.dims (id);
+    }
+    override void dimData (widgetID id, wdims d) {
+        changes.setDims(id, d);		// also updates WidgetDataSet in data.
+    }
+    
+    // These methods are only intended for use within the gui package. They are not necessarily
+    // thread-safe.
+    IRenderer renderer () {
+        assert (rend !is null, "WidgetManager.renderer: rend is null");
+        return rend;
+    }
+    
+    void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) {
+	debug assert (parnt && widg, "addPopup: null widget");
+        if (popups.length >= popupsMem.length)
+            popupsMem.length = popupsMem.length * 2 + 2;
+        with (popupsMem[popups.length]) {
+            parent = parnt;
+            widget = widg;
+	    w = widg.width;
+	    h = widg.height;
+            if (flags & 1) {
+	    	y = parent.yPos;
+            	if (y+h > this.h) y += parent.height - h;
+	    	x = parent.xPos + parent.width;
+	    	if (x+w > this.w) x = parent.xPos - w;
+            } else {
+                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 = popupsMem[0..popups.length+1];
+	requestRedraw;
+    }
+    void removePopup (IChildWidget parnt) {
+	foreach_reverse (i,popup; popups) {
+	    if (popup.parent is parnt)
+		popups = popups[0..i];
+	}
+	requestRedraw;
+    }
+
+    void requestRedraw () {
+        imde.mainSchedule.request(imde.SCHEDULE.DRAW);
+    }
+    
+    void addClickCallback (bool delegate(wdabs, wdabs, ubyte, bool) dg) {
+        clickCallbacks[dg.ptr] = dg;
+    }
+    void addMotionCallback (void delegate(wdabs, wdabs) dg) {
+        motionCallbacks[dg.ptr] = dg;
+    }
+    void removeCallbacks (void* frame) {
+        clickCallbacks.removeKey(frame);
+        motionCallbacks.removeKey(frame);
+    }
+    //END IWidgetManager methods
+    
 protected:
     /** Second stage of loading the widgets.
     * 
@@ -525,24 +364,116 @@
     void preSave ();
     
 public:
-    //BEGIN IWidgetManager methods
-    override IChildWidget makeWidget (widgetID id, IContent content = null) {
-        debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"');
-        return createWidget (this, id, curData[id], content);
-    }
+    //BEGIN makeWidget metacode
+private static {
+/// Widget types. Items match widget names without the "Widget" suffix.
+enum WIDGET_TYPE : int {
+    FUNCTION		= 0x2000,   // Function called instead of widget created (no "Widget" appended to fct name)
+    TAKES_CONTENT	= 0x4000,   // Flag indicates widget's this should be passed an IContent reference.
+    SAFE_RECURSION	= 0x8000,   // Safe to instantiate recursively without infinite looping.
+    
+    // Use widget names rather than usual capitals convention
+    Unnamed		= 0x0,      // Only for use by widgets not created with createWidget
+    
+    // blank: 0x1
+    FixedBlank		= 0x1,
+    SizableBlank	= 0x2,
+    Debug		= 0xF,
+    
+    // popup widgets: 0x10
+    PopupMenu		= TAKES_CONTENT | 0x11,
+    SubMenu		= TAKES_CONTENT | 0x12,
+    
+    // labels: 0x20
+    ContentLabel	= TAKES_CONTENT | 0x20,
+    TextLabel		= 0x21,
+    
+    // content functions: 0x30
+    editContent		= FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x30,
+    addContent		= FUNCTION | 0x31,
+    flatMenuContent	= FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x32,
+    subMenuContent	= FUNCTION | TAKES_CONTENT | 0x33,
+    
+    // content widgets: 0x40
+    DisplayContent	= TAKES_CONTENT | 0x40,
+    BoolContent		= TAKES_CONTENT | 0x41,
+    AStringContent	= TAKES_CONTENT | 0x42,
+    ButtonContent	= TAKES_CONTENT | 0x43,
+    MenuButtonContent	= TAKES_CONTENT | 0x44,
+    
+    GridLayout		= TAKES_CONTENT | 0x100,
+    ContentList		= TAKES_CONTENT | SAFE_RECURSION | 0x110,
     
-    override wdims dimData (widgetID id) {
-        return curData.dims (id);
+    FloatingArea	= 0x200,
+}
+
+// Only used for binarySearch algorithm generation; must be ordered by numerical values.
+const char[][] WIDGETS = [
+        "FixedBlank",
+        "SizableBlank",
+        "Debug",
+	"TextLabel",
+	"FloatingArea",
+	"addContent",
+	"PopupMenu",
+	"SubMenu",
+	"ContentLabel",
+        "DisplayContent",
+        "BoolContent",
+	"AStringContent",
+	"ButtonContent",
+	"MenuButtonContent",
+	"GridLayout",
+	"subMenuContent",
+	"ContentList",
+	"editContent",
+	"flatMenuContent"];
+
+/* Generates a binary search algorithm for makeWidget. */
+char[] binarySearch (char[] var, char[][] consts) {
+    if (consts.length > 3) {
+        return `if (`~var~` <= WIDGET_TYPE.`~consts[$/2 - 1]~`) {` ~
+                binarySearch (var, consts[0 .. $/2]) ~
+                `} else {` ~
+                binarySearch (var, consts[$/2 .. $]) ~
+                `}`;
+    } else {
+        char[] ret;
+        foreach (c; consts) {
+            ret ~= `if (` ~ var ~ ` == WIDGET_TYPE.` ~ c ~ `) {
+                        debug (mdeWidgets) logger.trace ("Creating new `~c~`.");
+                        if (!(WIDGET_TYPE.`~c~` & WIDGET_TYPE.SAFE_RECURSION))
+                    	  parent.recursionCheck (id);
+                        static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.FUNCTION)
+                          return `~c~` (this, parent, id, data, content);
+                        else static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.TAKES_CONTENT)
+                          return new `~c~`Widget (this, parent, id, data, content);
+                        else
+                          return new `~c~`Widget (this, parent, id, data);
+                    } else `;
+        }
+        ret = ret[0..$-6];	// remove last else
+        return ret;
     }
-    override void setData (widgetID id, WidgetData d) {
-        changes[id] = d;        // also updates WidgetDataSet in data.
+}
+
+debug { // check items in WIDGETS are listed in order
+    char[] WIDGETS_check () {
+        char[] ret;
+        for (int i = WIDGETS.length-2; i > 0; --i) {
+            ret ~= "WIDGET_TYPE."~WIDGETS[i] ~" >= WIDGET_TYPE."~ WIDGETS[i+1];
+            if (i>1) ret ~= " || ";
+        }
+        return ret;
     }
-    override void setDimData (widgetID id, wdims d) {
-        changes.setDims(id, d);    // also updates WidgetDataSet in data.
-    }
-    //END IWidgetManager methods
+    mixin ("static if ("~WIDGETS_check~")
+        static assert (false, \"WIDGETS is not in order!\");");
+}
+}
+    //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
@@ -555,10 +486,27 @@
     scope mt.DataSet changesDS;		// changes and sections from user file (used for saving)
     bool loadUserFile = true;		// still need to load user file for saving?
     
+    IRenderer rend;
+    
+    // Widgets:
     wdim w,h;				// area available to the widgets
     wdim mw,mh;				// minimal area required by widgets (ideally for limiting w,h)
     scope IChildWidget child;		// The primary widget.
     uint setupN;			// n to pass to IChildWidget.setup
     
+    struct ActivePopup {
+        IChildWidget widget;
+	IChildWidget parent;
+        wdabs x,y;
+        wdsize w,h;
+    }
+    ActivePopup[] popups;	// Pop-up [menus] to draw. Last element is top popup.
+    ActivePopup[] popupsMem;	// allocated memory for popups
+    // 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
+    
     Mutex mutex;			// lock on methods for use outside the package.
 }