view mde/content/AStringContent.d @ 126:c9843fbaac88

Dynamic minimal size changing improved; works over layouts sharing alignment. EnumContent sub-contents use EnumValueContent instead of BoolContent; fixes a few small bugs. EnumContent substrings get translated (bug fixed). The widget manager no longer attempts to set widget sizes smaller than their minimals, even though some will not be shown. SwitchWidget: has fixed sizableness now.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 08 Jan 2009 13:05:44 +0000
parents a2ef6b549101
children 41582439a42b
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) {
        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
    }
    
    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;
    }
    
protected:
    EnumValueContent[] enums;
    
    /** 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;
    }
}