view mde/gui/WidgetManager.d @ 171:7f7b2011b759

Partially complete commit: code runs but context menus don't work. Moved WMScreen.createRootWidget to WidgetManager.createWidgets. Put childContext under a popupHandler widget. TODO: implement IChildWidget.setContent(Content) (see AParentWidget.d:237).
author Diggory Hardy <diggory.hardy@gmail.com>
date Sun, 26 Jul 2009 11:04:17 +0200
parents e45226d3deae
children a1ba9157510e
line wrap: on
line source

/* 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 gui manager class base.
 *
 * 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.
 * 
 * Public non IWidget* methods should be thread-safe.
 *****************************************************************************/
module mde.gui.WidgetManager;

import mde.gui.WidgetDataSet;
import mde.gui.widget.Ifaces;
import mde.gui.renderer.createRenderer;

import imde = mde.imde;
import mde.content.Content;
import mde.content.ServiceContent;
debug import mde.content.miscContent;	// Debug menu
debug import mde.content.Debug;

// Widgets to create:
import mde.gui.widget.layout;
import mde.gui.widget.miscWidgets;
import mde.gui.widget.TextWidget;
import mde.gui.widget.contentFunctions;
import mde.gui.widget.miscContent;
import mde.gui.widget.Floating;
import mde.gui.widget.ParentContent;
import mde.gui.widget.AParentWidget;

public import tango.core.sync.Mutex;
import tango.util.log.Log : Log, Logger;
import tango.util.container.SortedMap;

private Logger logger;
static this () {
    logger = Log.getLogger ("mde.gui.WidgetManager");
}

/******************************************************************************
 * 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 manager.
     *
     * Params:
     *	name = The file name of the config for this GUI (to identify multiple GUIs). */
    protected this (char[] name) {
        auto p = "MiscOptions.l10n" in Content.allContent;
        assert (p, "MiscOptions.l10n not created!");
        p.addCallback (&reloadStrings);
	
	serviceContent = ServiceContentList.createItems (name);
	assert (cast (IServiceContent) Content.get ("menus.services."~name));
	
        debug {	// add a debug-mode menu
            auto lWS = new EventContent ("menus.debug."~name~".logWidgetSize");
            lWS.addCallback (&logWidgetSize);
        }
    }
    
public:
    //BEGIN IParentWidget methods
    // If call reaches the widget manager there isn't any recursion.
    //NOTE: should be override
    final void recursionCheck (widgetID, IContent) {}
    
    override void minWChange (IChildWidget widget, wdim nmw) {
	if (widget !is childRoot)	// Probably because widget is a popup widget
	    // This may get called from a CTOR, hence we can't check widget is one of childContext, etc.
	    return;
        mw = nmw;
        if (w < nmw) {
            childRoot.setWidth (nmw, -1);
            w = nmw;
        }
        childRoot.setPosition (0,0);
        requestRedraw;
    }
    override void minHChange (IChildWidget widget, wdim nmh) {
	if (widget !is childRoot)
	    return;
        mh = nmh;
        if (h < nmh) {
            childRoot.setHeight (nmh, -1);
            h = nmh;
        }
        childRoot.setPosition (0,0);
        requestRedraw;
    }
    //END IParentWidget methods
    
    //BEGIN IWidget methods
    override bool saveChanges () {
	bool ret = childRoot.saveChanges;
	ret |= childContext.saveChanges;
	if (childDragged !is null)
	    ret |= childDragged.saveChanges;
	return ret;
    }
    
    override bool dropContent (IContent content) {
	return false;
    }
    //END IWidget methods
    
    //BEGIN IPopupParentWidget methods
    override IPopupParentWidget getParentIPPW () {
        return this;
    }
    
    override void addChildIPPW (IPopupParentWidget ippw) {
	requestRedraw;
	if (ippw is childContext) {	// special handling - a separate IPPW
	    contextActive = true;
	    return;
	}
        if (childIPPW)
            childIPPW.removedIPPW;
        childIPPW = ippw;
    }
    override bool removeChildIPPW (IPopupParentWidget ippw) {
        if (childIPPW !is ippw) return false;
        childIPPW.removedIPPW;
        childIPPW = null;
        mAIPPW = MenuPosition.INACTIVE;
        requestRedraw;
        return false;
    }
    
    override void menuActive (MenuPosition mA) {
        mAIPPW = mA;
        if (childIPPW)
            childIPPW.menuActive = mA;
	if (contextActive)
	    childContext.menuActive = mA;
    }
    override MenuPosition menuActive () {
        return mAIPPW;
    }
    override MenuPosition parentMenuActive () {
        return MenuPosition.INACTIVE;
    }
    
    // Note: also triggered by non-popup widgets
    override void menuDone () {}
    
    override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) {
	IChildWidget ret;
	// Don't bother with childDragged; it has no interaction
	if (contextActive) {
	    ret = childContext.getPopupWidget (cx, cy, closePopup);
	    if (ret) return ret;
	    if (closePopup) {
		childContext.removedIPPW;
		requestRedraw;
	    }
        }
        if (childIPPW) {
            ret = childIPPW.getPopupWidget (cx, cy, closePopup);
            if (ret) return ret;
            if (closePopup) {
                removeChildIPPW (childIPPW);
            }
        }
        return null;
    }
    
    override void drawPopup () {
	if (contextActive)
	    childContext.draw();
	if (childDragged)
	    childDragged.draw();
    }
    
    debug protected override bool isChild (IPopupParentWidget ippw) {
	if (contextActive && ippw is childContext)
	    return true;
        return ippw is childIPPW;
    }
    
    override void removedIPPW () {}	// irrelevant
    //END IPopupParentWidget 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, parent, id, data, content);
    }
    
    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.
    }
    
    IRenderer renderer () {
        assert (rend !is null, "WidgetManager.renderer: rend is null");
        return rend;
    }
    
    MenuPosition positionPopup (IChildWidget parent, IChildWidget popup, MenuPosition position = MenuPosition.INACTIVE) {
	debug assert (parent && popup, "positionPopup: null widget");
	debug if (Debug.logPopupPositioning())
	    logger.trace ("Placing popup {} in relation to parent {}; input position: {}", popup, parent, position);
        wdim w = popup.width,
             h = popup.height,
             x, y;
        if (position & MenuPosition.ACTIVE) {
            y = parent.yPos;				// height flush with top
            if (y+h > this.h) y += parent.height - h;	// or bottom
	    if (position & MenuPosition.LEFT) {		// previously left
		x = parent.xPos - w;			// on left
		if (x < 0) {
		    x = parent.xPos + parent.width;	// on right
		    position = MenuPosition.RIGHT;
		}
	    } else {					// previously right or above/below
		x = parent.xPos + parent.width;		// on right
		position = MenuPosition.RIGHT;
		if (x+w > this.w) {
		    x = parent.xPos - w;		// or left
		    position = MenuPosition.LEFT;
		}
	    }
        } else {
	    wdim pw = parent.width;
	    if (popup.minWidth <= pw)
		popup.setWidth (pw, -1);		// neatness
	    x = parent.xPos;				// align on left edge
            if (x+w > this.w) x += pw - w;		// align on right edge
            y = parent.yPos + parent.height;		// place below
            if (y+h > this.h) y = parent.yPos - h;	// or above
	    position = MenuPosition.ACTIVE;
        }
        if (x < 0) x = 0;	// may be placed partially off-screen
        if (y < 0) y = 0;
        popup.setPosition (x, y);
	debug if (Debug.logPopupPositioning())
	    logger.trace ("Placed popup {}; output position: {}", popup, position);
	return position;
    }

    void requestRedraw () {
        imde.mainSchedule.request(imde.SCHEDULE.DRAW);
    }
    //END IWidgetManager methods
    
    debug void logWidgetSize (IContent) {
        logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh);
	logger.trace ("childRoot:");
	childRoot.logWidgetSize;
	logger.trace ("childContext:");
	childContext.logWidgetSize;
	if (childDragged !is null) {
	    logger.trace ("childDragged:");
	    childDragged.logWidgetSize;
	}
    }
    
protected:
    // These methods are called by derived classes to do the widget-management work
    //BEGIN WidgetManagement methods
    /** Second stage of widget loading.
     *
     * Widget data should be loaded before this is called. */
    final void createWidgets () {
        // The renderer needs to be created on the first load, but not after this.
        if (rend is null)
            rend = createRenderer (rendName);
        
        debug (mdeWidgets) logger.trace ("Creating root widget...");
        childRoot = makeWidget (this, "root");
        debug (mdeWidgets) logger.trace ("Setting up root widget...");
        childRoot.setup (0, 3);
        
        mw = childRoot.minWidth;
        mh = childRoot.minHeight;
	matchMinimalSize ();
        
        debug (mdeWidgets) logger.trace ("Setting size and position of root widget...");
        childRoot.setWidth  (w, -1);
        childRoot.setHeight (h, -1);
        childRoot.setPosition (0,0);
        debug (mdeWidgets) logger.trace ("Done creating root widget.");
	
	childContext = new PopupHandlerWidget (this, this, "contextHandler", "context", serviceContent);
	childContext.setup (0,3);
	debug (mdeWidgets) logger.trace ("Created context handler widget.");
	
	underMouse = childRoot;	// must be something
    }
    
    /** Draw all widgets */
    final void wmDrawWidgets() {
	if (childRoot)
	    childRoot.draw;
	if (childIPPW)
	    childIPPW.drawPopup;
	drawPopup;
    }
    
    /** For mouse click events.
     *
     * Sends the event on to the relevant windows and all click callbacks. */
    final void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) {
	if (childRoot is null) return;
	
	// Update underMouse to get the widget clicked on
	updateUnderMouse (cx, cy, state);
	
	// end of a drag?
	if (dragStart !is null && b == dragButton && state == false) {
	    IChildWidget dS = dragStart;
	    dragStart = null;
	    childDragged = null;
	    requestRedraw;
	    if (dS.dragRelease (cx, cy, underMouse))
		return;
	}
	
	// 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
	    Content contextContent = cast(Content)underMouse.content;
	    if (contextContent !is null) {
		serviceContent.setContent (contextContent);
		childContext.openMenu (underMouse, contextContent);
	    }
	} else {	// post other button presses to clickEvent
	    int ret = underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state);
	    if (ret & 1) {	// keyboard input requested
		keyFocus = underMouse;
		setLetterCallback (&underMouse.keyEvent);
	    }
	    if (ret & 2 && dragStart is null) {	// drag events requested
		dragStart = underMouse;
		dragButton = b;	// currently we allow any button to be used for a drag, but.. ?
		if (ret & 4) {
		    IContent c = underMouse.content();
		    if (c) {	// NOTE: creates a new widget, not optimal
			childDragged = new DisplayContentWidget (this, this, "dragContentDisplay", WidgetData ([0], []), c);
			childDragged.setup (0, 3);
			dragX = underMouse.xPos - cx;
			dragY = underMouse.yPos - cy;
			childDragged.setPosition (cx + dragX, cy + dragY);
		    }
		}
	    }
	}
    }
    
    /** For mouse motion events.
     *
     * Lock on mutex before calling. Pass new mouse coordinates. */
    final void wmMouseMotion (wdabs cx, wdabs cy) {
	updateUnderMouse (cx, cy, false);
	
	if (dragStart !is null) {
	    dragStart.dragMotion (cx, cy, underMouse);
	    if (childDragged !is null) {
		childDragged.setPosition (cx + dragX, cy + dragY);
		requestRedraw;
	    }
	}
    }
    
    
    /** A change callback on MiscOptions.l10n content to update widgets.
     *
     * Relies on another callback reloading translations to content first! */
    final void reloadStrings (IContent) {
        synchronized(mutex) {
            if (childRoot is null) return;
            childRoot.setup (++setupN, 2);
            childRoot.setWidth  (w, -1);
            childRoot.setHeight (h, -1);
            childRoot.setPosition (0,0);
	    childContext.setup (setupN, 2);
	    //TODO: possibly childDragged?
            requestRedraw;
        }
    }
    // for internal use
    final void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) {
        auto oUM = underMouse;
        underMouse = getPopupWidget (cx, cy, closePopup);
        if (underMouse is null) {
            debug assert (childRoot.onSelf (cx, cy), "WidgetManager: childRoot doesn't cover whole area");
            underMouse = childRoot.getWidget (cx, cy);
        }
        if (underMouse !is oUM) {
            debug assert (oUM && underMouse, "no widget under mouse: error");
            oUM.underMouse (false);
            underMouse.underMouse (true);
	    debug if (Debug.logUnderMouse())
		logger.trace ("Widget under mouse: {}", underMouse);
        }
    }
    
    /** If possible, the screen-interaction derived class should override to
     * make sure the window is at least (mw,mh) in size. In any case, this
     * method MUST make sure w >= mw and h >= mh even if the window isn't this
     * big.
     * 
     * A resize may not be required when this is called, however. */
    void matchMinimalSize () {
	if (w < mw) {
	    logger.warn ("Min width for gui, {}, not met: {}", mw, w);
	    w = mw;
	}
	if (h < mh) {
	    logger.warn ("Min height for gui, {}, not met: {}", mh, h);
	    h = mh;
	}
    }
    
    /// 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
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.
    
    // 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		= TAKES_CONTENT | 0xF,
    
    // popup widgets: 0x10
    PopupMenu		= TAKES_CONTENT | 0x11,
    
    // labels: 0x20
    TextLabel		= 0x21,
    
    // content functions: 0x30
    editContent		= FUNCTION | TAKES_CONTENT | 0x30,
    addContent		= FUNCTION | 0x31,
    popupListContent	= FUNCTION | TAKES_CONTENT | 0x33,
    
    // content widgets: 0x40
    DisplayContent	= TAKES_CONTENT | 0x40,
    BoolContent		= TAKES_CONTENT | 0x41,
    AStringContent	= TAKES_CONTENT | 0x42,
    ButtonContent	= TAKES_CONTENT | 0x43,
    SliderContent	= TAKES_CONTENT | 0x44,
    
    GridLayout		= TAKES_CONTENT | 0x100,
    ContentList		= TAKES_CONTENT | 0x110,
    
    FloatingArea	= TAKES_CONTENT | 0x200,
    Border		= TAKES_CONTENT | 0x204,
    Switch		= TAKES_CONTENT | 0x210,
    Collapsible		= TAKES_CONTENT | 0x214,
}

// Only used for binarySearch algorithm generation; must be ordered by numerical values.
const char[][] WIDGETS = [
        "FixedBlank",
        "SizableBlank",
	"TextLabel",
	"addContent",
        "Debug",
	"PopupMenu",
        "DisplayContent",
        "BoolContent",
	"AStringContent",
	"ButtonContent",
	"SliderContent",
	"GridLayout",
	"ContentList",
	"FloatingArea",
	"Border",
	"Switch",
	"Collapsible",
	"editContent",
	"popupListContent"];

/* 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~`.");
                        parent.recursionCheck (id, content);
                        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;
    }
}

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;
    }
    mixin ("static if ("~WIDGETS_check~")
        static assert (false, \"WIDGETS is not in order!\");");
}
}
    //END makeWidget metacode
    
protected:
    // Main child widget:
    IChildWidget childRoot;		// Root of the main GUI widget tree
    
    // Dimensions and child set-up data (fit to childRoot):
    wdim w,h;				// current widget size; should be at least (mw,mh) even if not displayable
    wdim mw,mh;				// minimal area required by widgets
    uint setupN;			// n to pass to IChildWidget.setup
    
    // IPopupParentWidget stuff for childRoot:
    MenuPosition mAIPPW;		// IPPW variable
    IPopupParentWidget childIPPW;	// child IPPW, if any active
    
    IChildWidget keyFocus;		// widget receiving keyboard input
    IChildWidget underMouse;		// widget under the mouse pointer
    
    
    // Context menu:
    // Essentially, we consider childContext a full child IPPW, but handle it separately from
    // childIPPW. Instead of providing another ref. for this IPPW, shortcut by using this reference
    // and the boolean contextActive:
    scope PopupHandlerWidget childContext;	// context menu popup (handler)
    bool contextActive = false;		// If true, consider childContext a child IPPW
    scope IServiceContent serviceContent;	// context menu content tree
    
    
    // Drag-and-drop data:
    //NOTE: could be wrapped with a PopupHandlerWidget, but can't set position then?
    scope IChildWidget childDragged;	// displays dragged content; no interaction
    IChildWidget dragStart;		// if non-null, this widget should receive motion and click-release events
    int dragButton;			// index of button in use for drag
    wdrel dragX, dragY;			// coordinates of dragged content relative to mouse
    
    
    // Renderer:
    char[] rendName;			// Name of renderer; for saving and creating renderers
    scope IRenderer rend;
    
    
    // Data loaded/to save:
    WidgetDataSet curData;		// Current data
    WidgetDataChanges changes;		// Changes for the current design.
    
    Mutex mutex;			// lock on methods for use outside the package.
}