view mde/gui/WidgetManager.d @ 167:620d4ea30228

Context menus: added a clipboard (functions accessible from main menu rather than context menu).
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 27 Jun 2009 11:57:26 +0200
parents 55667d048c31
children da8d3091fdaf
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 imde = mde.imde;
import mde.content.Content;
import mde.content.ServiceContent;
debug import mde.content.miscContent;	// Debug menu

// 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;

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 = cast (IServiceContent) Content.get ("menus.services");
	assert (Content.get ("menus.services"));
	assert (serviceContent !is null, "Content service menu doesn't exist or has wrong type");
	
        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 child)	// Usually because widget is a floating widget
	    // This may get called from a CTOR, hence we can't check widget is one of popupContext, etc.
	    return;
        mw = nmw;
        if (w < nmw) {
            child.setWidth (nmw, -1);
            w = nmw;
        }
        child.setPosition (0,0);
        requestRedraw;
    }
    override void minHChange (IChildWidget widget, wdim nmh) {
	if (widget !is child)
	    return;
        mh = nmh;
        if (h < nmh) {
            child.setHeight (nmh, -1);
            h = nmh;
        }
        child.setPosition (0,0);
        requestRedraw;
    }
    
    override bool dropContent (IContent content) {
	return false;
    }
    //END IParentWidget methods
    
    //BEGIN IPopupParentWidget methods
    override IPopupParentWidget getParentIPPW () {
        return this;
    }
    
    override void addChildIPPW (IPopupParentWidget ippw) {
        if (childIPPW)
            childIPPW.removedIPPW;
        childIPPW = ippw;
        requestRedraw;
    }
    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;
    }
    override MenuPosition menuActive () {
        return mAIPPW;
    }
    override MenuPosition parentMenuActive () {
        return MenuPosition.INACTIVE;
    }
    
    // Don't do anything. E.g. can get called by non-popup buttons.
    override void menuDone () {}
    
    override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) {
        if (popupContext) {
            if (popupContext.onSelf (cx, cy))
            	return popupContext;
            if (closePopup) {
            	if (childIPPW is null)
		    menuActive = MenuPosition.INACTIVE;
            	popupContext = null;
                requestRedraw;
            }
        }
        if (childIPPW) {
            IChildWidget ret =
                    childIPPW.getPopupWidget (cx, cy, closePopup);
            if (ret) return ret;
            if (closePopup) {
                menuActive = MenuPosition.INACTIVE;
                removeChildIPPW (childIPPW);
            }
        }
        return null;
    }
    
    override void drawPopup () {
        if (popupContext)
            popupContext.draw();
	if (dragContentDisplay)
	    dragContentDisplay.draw();
    }
    
    debug protected override bool isChild (IPopupParentWidget ippw) {
        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");
        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 logger.trace ("placed popup at {},{}; size: {},{}", x,y, w,h);
	return position;
    }

    void requestRedraw () {
        imde.mainSchedule.request(imde.SCHEDULE.DRAW);
    }
    //END IWidgetManager methods
    
    debug void logWidgetSize (Content) {
        logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh);
        child.logWidgetSize;
    }
    
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;
	
	// 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;
	    dragContentDisplay = 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) return;
	    // NOTE: Creates new widgets every time; not optimal
	    serviceContent.setContent (contextContent);
	    popupContext = makeWidget (this, "context", contextContent);
	    popupContext.setup (0, 3);
	    positionPopup (underMouse, popupContext);
	    requestRedraw;
	} 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
			dragContentDisplay = new DisplayContentWidget (this, this, "dragContentDisplay", WidgetData ([0], []), c);
			dragContentDisplay.setup (0, 3);
			dragX = underMouse.xPos - cx;
			dragY = underMouse.yPos - cy;
			dragContentDisplay.setPosition (cx + dragX, cy + dragY);
		    }
		}
	    }
	}
    }
    
    /** For mouse motion events.
     *
     * Lock on mutex before calling. Pass new mouse coordinates. */
    void wmMouseMotion (wdabs cx, wdabs cy) {
	updateUnderMouse (cx, cy, false);
	
	if (dragStart !is null) {
	    dragStart.dragMotion (cx, cy, underMouse);
	    if (dragContentDisplay !is null) {
		dragContentDisplay.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! */
    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);
        if (underMouse is null) {
            debug assert (child.onSelf (cx, cy), "WidgetManager: child doesn't cover whole area");
            underMouse = child.getWidget (cx, cy);
        }
        if (underMouse !is oUM) {
            debug assert (oUM && underMouse, "no widget under mouse: error");
            oUM.underMouse (false);
            underMouse.underMouse (true);
        }
    }
    
    /// 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:
    WidgetDataSet curData;		// Current data
    WidgetDataChanges changes;		// Changes for the current design.
    
    char[] rendName;			// Name of renderer; for saving and creating renderers
    IRenderer rend;
    
    // Widgets:
    wdim w,h;				// current widget size; should be at least (mw,mh) even if not displayable
    wdim mw,mh;				// minimal area required by widgets
    scope IChildWidget child;		// The primary widget.
    uint setupN;			// n to pass to IChildWidget.setup
    
    MenuPosition mAIPPW;		// IPPW variable
    IPopupParentWidget childIPPW;	// child IPPW, if any active
    
    // Popup(s) handled directly by AWidgetManager:
    IChildWidget popupContext;		// context menu (active if not null)
    IServiceContent serviceContent;	// context menu content tree
    IChildWidget dragContentDisplay;	// 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
    
    IChildWidget keyFocus;		// widget receiving keyboard input
    IChildWidget underMouse;		// widget under the mouse pointer
    
    Mutex mutex;			// lock on methods for use outside the package.
}