view mde/content/AStringContent.d @ 124:a2ef6b549101

Dynamic minimal size changing is now fully supported. Support for reducing minimal size in layouts. Editing numbers as text now always converts new number back to string at end of edit. Floating point number display format changed.
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 05 Jan 2009 12:43:27 +0000
parents d3b2cefd46c9
children c9843fbaac88
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);
    }
    
    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. */
// maybe avoid problems by adding a special callback to BoolContent subclass?
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;
        foreach (i, ref e; enums) {
            e = new BoolContent (enumSymbols[i], false);
            e.addCallback (&enumAssignCb);
        }
	super (symbol, val);
        //NOTE: if val is 0, no enumeration should be set; however it seems to work!
    }
    
    override void assignNoCb (int val) {
        if (val == v) return;
        if (val >= enums.length || val < 0) {
	    logger.error ("EnumContent "~name_~" assigned invalid value; keeping value: "~sv);
	    return;
	}
        enums[v] = false;
        enums[val] = true;
	v = val;
        svAssign;
    }
    void svAssign () {
	sv = enums[v].name_;
	if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    
    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:
    void enumAssignCb (Content c) {
        if (enums[v] is c) {	// No other is set so persist value
            enums[v].assignNoCb (true);
        }
        foreach (i,e; enums)
            if (c is e) {
            	enums[v].assignNoCb = false;	// NOTE: opAssign causes sigsegv (due to recursive calling?)
                v = i;
                svAssign;
                return;
            }
        debug logger.error ("EnumContent.enumAssignCb: invalid value (code error)");
    }
    
    BoolContent[] enums;	// NOTE: an array isn't always fastest
}