view mde/content/AStringContent.d @ 128:41582439a42b

Added support for dynamic EnumContent loading and saving, with translation loading. WMScreen.init removed; code moved to this() since class is now created by main() instead of a static this(). Fix for SwitchWidget not passing events. Still some resizing bugs evident in SwitchWidget :-(
author Diggory Hardy <diggory.hardy@gmail.com>
date Wed, 14 Jan 2009 20:24:14 +0000
parents c9843fbaac88
children 9f035cd139c6
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 content system − string-based editable content.
 */
module mde.content.AStringContent;
public import mde.content.Content;

//FIXME: efficient conversions? Need to dup result when formatting a string anyway?
import Int = tango.text.convert.Integer;
import Float = tango.text.convert.Float;
import derelict.sdl.keysym;

import tango.util.log.Log : Log, Logger;
private Logger logger;
static this () {
    logger = Log.getLogger ("mde.content.AStringContent");
}

// Used by Options
template ContentN(T) {
    static if (is(T == bool)) {
	const char[] ContentN = "BoolContent";
    } else static if (is(T == int)) {
	const char[] ContentN = "IntContent";
    } else static if (is(T == double)) {
	const char[] ContentN = "DoubleContent";
    } else static if (is(T == char[])) {
	const char[] ContentN = "StringContent";
    } else
	static assert (false, "No Content of type "~T.stringof);
}

/** Base class for content containing a simple value editable as text. All content types used by
 * Options extend this class.
 *
 * All derived classes should have the following functions:
 * ---
 *  char[] endEdit ();	// Should convert sv and assign to self, then call endEvent
 * // Used by Options:
 *  BoolContent changeCb (void delegate (char[] symbol,T value) cb);	// The callback used by Options
 *  void assignNoCb (T val);	// assign val, but without calling callbacks
 * ---
 * On any assignation (by this, assignNoCb, opAssign) the value should be converted to a string and
 * assigned to sv, and pos should be clamped to [0,sv.length] (i.e. enforce pos <= sv.length). */
abstract class AStringContent : Content
{
    protected this (char[] symbol) {
	super (symbol);
    }
    
    /// Get the text.
    override char[] toString (uint i) {
        return i == 0 ? sv
             : i == 1 ? name_
             : i == 2 ? desc_
             : null;
    }
    
    /** Acts on a keystroke and returns the new value.
     *
     * Supports one-line editing: left/right, home/end, backspace/delete. */
    char[] keyStroke (ushort sym, char[] i) {
	debug assert (i.length, "StringContent.keyStroke: no value (??)");	// impossible?
	char k = *i;
	if (k >= 0x20) {
	    if (k == 0x7f) {		// delete
		size_t p = pos;
		if (p < sv.length) ++p;
		while (p < sv.length && (sv[p] & 0x80) && !(sv[p] & 0x40))
		    ++p;
		sv = sv[0..pos] ~ sv[p..$];
	    } else {			// insert character
		char[] tail = sv[pos..$];
		sv.length = sv.length + i.length;
		size_t npos = pos+i.length;
		if (tail) sv[npos..$] = tail.dup;	// cannot assign with overlapping ranges
		    sv[pos..npos] = i;
		pos = npos;
	    }
	} else {			// use sym; many keys output 0
            if (sym >= SDLK_RSHIFT &&  (sym <= SDLK_COMPOSE || sym == SDLK_MENU)) {
            }	// all modifier keys and contect menu key; should be ignored
	    else if (sym == SDLK_BACKSPACE) {	// backspace; k == 0x8
		char[] tail = sv[pos..$];
		if (pos) --pos;
		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
		    --pos;
		sv = sv[0..pos] ~ tail;
	    } else if (sym == SDLK_LEFT) {
		if (pos) --pos;
		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
		    --pos;
	    } else if (sym == SDLK_RIGHT) {
		if (pos < sv.length) ++pos;
		while (pos < sv.length && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
		    ++pos;
	    } else if (sym == SDLK_HOME || sym == SDLK_UP) {
		pos = 0;
	    } else if (sym == SDLK_END || sym == SDLK_DOWN) {
		pos = sv.length;
	    } else
		debug logger.trace ("Unhandled symbol: {}", sym);
	}
	return sv;
    }
    
    /** Get the character the edit cursor is in front of.
     *
     * NOTE: unused. */
    size_t editIndex () {
	size_t i = 0;
	for (size_t p = 0; p < pos; ++p)
	    if (!(sv[p] & 0x80) || sv[p] & 0x40)
		++i;
	return i;
    }
    /** Set the edit position in front of i'th character.
     *
     * Assumes at least i characters are present; if not this will work but not be optimal. */
    void editIndex (size_t i) {
        pos = 0;
        for (; i > 0; --i) {	// NOTE: could be slightly optimised
            if (pos < sv.length) ++pos;
            while (pos < sv.length && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
                ++pos;
        }
    }
    
    /** Call after editing a string; return new string (may be changed/reverted). */
    char[] endEdit ();
    
protected:
    char[] sv;		// string of value; updated on assignment for displaying and editing
    size_t pos;		// editing position; used by keyStroke
}

class BoolContent : AStringContent
{
    /** Create a content with _symbol name symbol. */
    this (char[] symbol, bool val = false) {
	assignNoCb (val);
	super (symbol);
    }
    // for use by EnumValueContent
    protected this (char[] symbol, int dummy) {
        super (symbol);
    }
    
    void assignNoCb (bool val) {
	v = val;
	sv = v ? "true" : "false";
	if (pos > sv.length) pos = sv.length;
    }
    void opAssign (bool val) {
	assignNoCb (val);
	endEvent;
    }
    bool opCall () {
        return v;
    }
    alias opCall opCast;
    
    override char[] endEdit () {
	v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
        sv = v ? "true" : "false";
        endEvent;
	return sv;
    }
    
protected:
    bool v;
}

/** Text content. */
class StringContent : AStringContent
{
    this (char[] symbol, char[] val = null) {
        v = val;
	super (symbol);
    }
    
    void assignNoCb (char[] val) {
	v = val;
	if (pos > sv.length) pos = sv.length;
    }
    void opAssign (char[] val) {
	assignNoCb (val);
	endEvent;
    }
    char[] opCall () {
        return v;
    }
    alias opCall opCast;
    
    override char[] endEdit () {
	endEvent;
	return sv;
    }
    
protected:
    alias sv v;		// don't need separate v and sv in this case
}

/** Integer content. */
class IntContent : AStringContent
{
    /** Create a content with _symbol name symbol. */
    this (char[] symbol, int val = 0) {
        assignNoCb (val);
	super (symbol);
    }
    
    void assignNoCb (int val) {
	v = val;
	sv = Int.toString (v);
	if (pos > sv.length) pos = sv.length;
    }
    void opAssign (int val) {
	assignNoCb (val);
	endEvent;
    }
    int opCall () {
        return v;
    }
    alias opCall opCast;
    
    override char[] endEdit () {
	try {
            v = Int.toInt (sv);
        } catch (Exception e) {
            logger.warn (e.msg);
        }
        sv = Int.toString (v);
        endEvent;
	return sv;
    }
    
protected:
    int v = 0;	// must be a valid value for EnumContent (i.e. 0)
}

/** Double content. */
class DoubleContent : AStringContent
{
    /** Create a content with _symbol name symbol. */
    this (char[] symbol, double val = 0) {
        assignNoCb (val);
	super (symbol);
    }
    
    void assignNoCb (double val) {
	v = val;
	sv = Float.toString (v, 8, 4);
	if (pos > sv.length) pos = sv.length;
    }
    void opAssign (double val) {
	assignNoCb (val);
	endEvent;
    }
    double opCall () {
        return v;
    }
    alias opCall opCast;
    
    override char[] endEdit () {
        try {
            v = Float.toFloat (sv);
        } catch (Exception e) {
            logger.warn (e.msg);
        }
        sv = Float.toString (v, 8, 4);
        endEvent;
	return sv;
    }
    
protected:
    double v;
}

/** A content representing an enumeration.
 *
 * Extends IntContent for Options support. */
class EnumContent : IntContent, IContentList
{
    /** CTOR.
    *
    * Params:
    *	enumSymbols = Symbol names for each
    *	val = which value is active; must be in [0,length-1]
    */
    this (char[] symbol, char[][] enumSymbols, int val = 0) {
        this.enumSymbols = enumSymbols;
        enums.length = enumSymbols.length;
        char[] symPeriod = symbol~'.';
        foreach (i, ref e; enums) {
            e = new EnumValueContent (this, i, symPeriod~enumSymbols[i]);
        }
        super (symbol, val);	// calls assignNoCb
    }
    
    /** Create from a EnumCStruct. */
    this (char[] symbol, EnumCStruct data) {
        this (symbol, data.symbols, data.value);
    }
    
    /** Return an EnumCStruct, which could be used to recreate this Content. */
    //NOTE: save EnumCStruct directly?
    EnumCStruct structOf () {
        EnumCStruct r;
        r.symbols = enumSymbols;
        r.value = v;
        return r;
    }
    
    override void assignNoCb (int val) {	// called by children (via opAssign)
        if (val >= enums.length || val < 0) {
	    logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~sv);
	    return;
	}
        enums[v]  .assignFromParent (false);
        enums[val].assignFromParent (true);
	v = val;
        sv = enums[v].name_;
        if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    // Change if true, assert current value if false
    void childAssign (int val) {
        debug assert (0 <= val && val < enums.length, "cA out of bounds");
        if (enums[val].v)
            assignNoCb (val);
        else
            enums[val].assignFromParent (v == val);
    }
    
    override char[] endEdit () {
	foreach (i,e; enums)
	    if (sv == e.name_) {
		v = i;
		goto break1;
	    }
	
        sv = enums[v].name_;	// sv was edited; revert
        logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~sv);
	if (pos > sv.length) pos = sv.length;
	
	break1:
	endEvent;
	return sv;
    }
    
    override Content[] list () {
        return enums;
    }
    
    struct EnumCStruct {
        char[][] symbols;
        int value;
    }
    
protected:
    EnumValueContent[] enums;
    char[][] enumSymbols;	// saved for getStructOf
    
    /** Special version of BoolContent for each enumeration to update the parent Enum. */
    private class EnumValueContent : BoolContent {
        /** New enumeration of parent with index num. */
        this (EnumContent parent, size_t num, char[] symbol) {
            this.parent = parent;
            i = num;
            super (symbol, 0);	// parent sets value (if true); we shouldn't
            sv = "false";	// except for this
        }
        
        override void assignNoCb (bool val) {
            v = val;
            parent.childAssign (i);
        }
        void assignFromParent (bool val) {	// don't call back to parent
            super.assignNoCb (val);
            endEvent;
        }
        
        override char[] endEdit () {
            v = sv && (sv[0] == 't' || sv[0] == 'T' || sv[0] == '1');
            parent.childAssign (i);
            return sv;
        }
    
    protected:
        EnumContent parent;
        size_t i;
    }
}