view mde/gui/WidgetManager.d @ 117:aba2dd815a1f

Some tweaks to popup events and widgets. Moved gui.mtt to guiDemo.mtt Changed handling of clicks with popups. Made some of the popup widgets use usual from widget data construction.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 26 Dec 2008 12:07:38 +0000
parents 5ee69b3ed9c9
children 5b37d0400732
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.
 *
 * This is the module to use externally to create a graphical user interface (likely also with
 * content modules).
 *************************************************************************************************/
module mde.gui.WidgetManager;

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

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 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 () {
    logger = Log.getLogger ("mde.gui.WidgetManager");
}

/*************************************************************************************************
 * 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
{
    /** Construct a new widget loader.
    * 
    * params:
    *  fileName = Name of file specifying the gui, excluding path and extension.
    */
    protected this (char[] file) {
        mutex = new Mutex;  // Used on functions intended to be called from outside the gui package.
        fileName = file;
    }
    
    /* Load the widgets' data from the file specified to the CTOR.
    * 
    * params:
    *  allDesigns = Load all sections
    */
    private void loadData (bool allDesigns = false) {
        if (allLoaded || (defaultDesign !is null && allDesigns == false))
            return; // test if already loaded
            
            // Set up a reader
            scope IReader reader;
        try {
            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true);
            
            // Read from the HEADER:
            // Get the renderer
            char[]* p = "Renderer" in reader.dataset.header._charA;
            if (p is null || *p is null) {
                logger.warn ("No renderer specified: using \"Simple\"");
                rendName = "Simple";
            }
            else
                rendName = *p;
            
            // Get which section to use
            p = "Design" in reader.dataset.header._charA;
            if (p is null || *p is null) {
                logger.warn ("No gui design specified: trying \"Default\"");
                defaultDesign = "Default";
            }
            else
                defaultDesign = *p;
            
            // Read the body:
            // Load the chosen design
            reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
                WidgetDataSet* p = id in data;
                if (p is null) {
                    data[id] = new WidgetDataSet;
                    return *(id in data);
                }
                return *p;
            };
            
            if (allDesigns) {
                reader.read;
                allLoaded = true;
            } else
                reader.read([defaultDesign]);
        } catch (NoFileException) {
            logger.error ("Unable to load GUI: no config file: "~fileName);
            // just return: not a fatal error (so long as the game can run without a GUI!)
        } catch (Exception e) {
            logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):");
            logger.error (e.msg);
            throw new GuiException ("Failure parsing config file");
        }
    }
    
    /** Load the gui from some design.
    * 
    * If a design was previously loaded, its changes are saved first.
    * 
    * Params:
    *  name = Design to load. If null, the default will be loaded.
    */
    void loadDesign (char[] name = null) {
        if (changes !is null)	// A design was previously loaded
            save;       // own lock
            
	mutex.lock;
        scope(exit) mutex.unlock;
        
        // Load data (loadData tests if it's already loaded first):
        if (name is null) {
            loadData (false);
            name = defaultDesign;
        } else
            loadData (true);
        
        
        // Get data:
        auto p = name in data;
        while (p is null) {
            if (name == defaultDesign)
                throw new GuiException ("Unable to load [specified or] default design");
            name = defaultDesign;       // try again with the default
            p = name in data;
        }
        curData = *p;
        
        // Get/create a changes section:
        if (changesDS is null)
            changesDS = new mt.DataSet;
        
        mt.IDataSection* q = name in changesDS.sec;
        if (!q || ((changes = cast(WidgetDataChanges) *q) is null)) {
            changes = new WidgetDataChanges (curData);
            changesDS.sec[name] = changes;
        }
        
        // Create the widgets:
        createRootWidget;
    }
    
    /** Save changes, if any exist.
    * 
    * Is run when the manager is destroyed, but could be run at other times too. */
    void save () {
	preSave;
	
        mutex.lock;
        scope(exit) mutex.unlock;
        
        // Make all widgets save any changed data; return if no changes:
        if (!child.saveChanges)
            return;
        
        if (loadUserFile) { // merge entries from user file into current changes
            try {
                scope IReader reader = confDir.makeMTReader (
                fileName, PRIORITY.HIGH_ONLY, changesDS, true);
                
                // Create if necessary, only corresponding to existing designs read:
                reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) {
                    WidgetDataSet* p = id in data;
                    if (p is null)
                        throw new Exception ("File has changed since it was loaded!");
                    return new WidgetDataChanges (*p);
                };
                
                reader.read;
            } catch (NoFileException) {
                // No user file exists; not an error.
            } catch (Exception e) {
                logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:");
                logger.error (e.msg);
                logger.error ("Overwriting the file.");
                // Continue...
            }
            loadUserFile = false;   // don't need to do it again
        }
        
        try {   // Save
        IWriter writer;
        writer = confDir.makeMTWriter (fileName, changesDS);
        writer.write;
        } catch (Exception e) {
            logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:");
            logger.error (e.msg);
            // No point in throwing since it doesn't affect anything else.
        }
    }
    
    /** Get the names of all designs available. */
    char[][] designs() {
        synchronized(mutex) {
            loadData (true);
            return data.keys;
        }
    }
    
    /** Called when translation strings have been reloaded. */
    void reloadStrings (Content) {
	Items.loadTranslation;
	child.setup (++setupN, 2);
	child.setWidth  (w, -1);
	child.setHeight (h, -1);
	child.setPosition (0,0);
	requestRedraw;
    }
    
protected:
    /** Second stage of loading the widgets.
    * 
    * loadDesign handles the data; this method needs to:
    * ---
    * // 1. Create the root widget:
    * child = makeWidget ("root");
    * child.setup (0, 3);
    * // 2. Set the size:
    * child.setWidth  (child.minWidth,  1);
    * child.setHeight (child.minHeight, 1);
    * // 3. Set the position (necessary part of initialization):
    * child.setPosition (0,0);
    * ---
    */
    void createRootWidget();
    
    /** Called before saving (usually when the GUI is about to be destroyed, although not
    *  necessarily). */
    void preSave ();
    
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);
    }
    
    override wdims dimData (widgetID id) {
        return curData.dims (id);
    }
    override void setData (widgetID id, WidgetData d) {
        changes[id] = d;        // also updates WidgetDataSet in data.
    }
    override void setDimData (widgetID id, wdims d) {
        changes.setDims(id, d);    // also updates WidgetDataSet in data.
    }
    //END IWidgetManager methods
    
protected:
    final char[] fileName;
    char[] defaultDesign;		// The design specified in the file header.
    char[] rendName;			// Name of renderer; for saving and creating renderers
    
    // Loaded data, indexed by design name. May not be loaded for all gui designs:
    scope WidgetDataSet[char[]] data;
    private bool allLoaded = false;	// applies to data
    WidgetDataSet curData;		// Current data
    WidgetDataChanges changes;		// Changes for the current design.
    scope mt.DataSet changesDS;		// changes and sections from user file (used for saving)
    bool loadUserFile = true;		// still need to load user file for saving?
    
    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
    
    Mutex mutex;			// lock on methods for use outside the package.
}