view mde/content/AStringContent.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 bb2f1a76346d
children e45226d3deae
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;

import Ascii = tango.text.Ascii;
import Util = tango.text.Util;
//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 Math = tango.math.Math;
import derelict.sdl.keysym;

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

private {
    // Returns the length of memory to allocate, when len is the min required.
    // Returns at least 66, since formatting from int/float requires this.
    size_t allocLength (size_t len) {
	return len < 33 ? 66 : len * 2;
    }
}

/** Union of all content types here - for dynamic cast checking against several
 * types. */
union UnionContent {
    AStringContent asc;
    BoolContent bc;
    StringContent sc;
    IntContent ic;
    DoubleContent dc;
    EnumContent ec;
}

/** Base class for content containing a simple value editable as text.
 *
 * Derived classes should implement endEdit to convert sv and assign its value
 * to self, then call endEvent.
 *
 * On assignation by this or 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);
	svBuf.length = allocLength (0);
    }
    
    /// Get the text.
    override char[] toString (uint i) {
        return i == 0 ? sv
             : i == 1 ? name_
             : i == 2 ? desc_
             : null;
    }
    
    /** Set the content via conversion to/from string. */
    override bool set (IContent c) {
	if (c !is null) {
	    sv = c.toString (0).dup;
	    return endEdit;
	}
	return false;
    }
    
    /** Set value directly from a string. */
    override void opAssign (char[] str) {
	sv = str.dup;
	return endEdit;
    }
    
    /** Acts on a keystroke to edit the value as a string.
     *
     * After this has been used to edit the string, endEdit should be called
     * to convert back to the native value type.
     *
     * Supports one-line editing: left/right, home/end, backspace/delete. */
    char[] keyStroke (ushort sym, char[] i) {
	// This routine relies on the folowing: svBuf[0..sv.length] == sv
	if (sv.ptr !is svBuf.ptr) {
	    // sv may be within svBuf but not starting at its beginning
	    //NOTE: to further optimise, try to avoid a realloc on the heap
	    svBuf = new char[allocLength (sv.length)];
	    svBuf[0..sv.length] = sv;
	}
	//NOTE (optimise): dup is used several times for temporary storage
	//perhaps could be optimised by a dup func using thread-local buffer?
	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;
		size_t l = sv.length - (p-pos);
		sv[pos..l] = sv[p..$].dup;
		sv = sv[0..l];
	    } else {			// insert character
		char[] tail = sv[pos..$];
		size_t l = sv.length + i.length;
		if (svBuf.length < l)
		    svBuf.length = allocLength (l);
		size_t npos = pos+i.length;
		if (tail) svBuf[npos..l] = tail.dup;
		svBuf[pos..npos] = i;
		sv = svBuf[0..l];
		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..$];
		size_t l = pos;
		if (pos) --pos;
		while (pos && (sv[pos] & 0x80) && !(sv[pos] & 0x40))
		    --pos;
		l = sv.length - (l - pos);
		sv[pos..l] = tail.dup;
		sv = sv[0..l];
	    } 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.
     *
     * Returns: true if string successfully converted to value.
     * 
     * Should never throw; should reset sv at least when returning false. */
    bool endEdit ();
    
protected:
    //TODO: copy-on-assign, copy-on-edit, or what?
    /* String version of value (for toString(0) and editing).
     * WARNING: This must point to mutable memory (for endEdit), and
     * when keyStroke can be called we must have svBuf[0..sv.length] == sv. */
    char[] sv;
    char[] svBuf;	// buffer to reduce reallocs
    size_t pos;		// editing position; used by keyStroke
}

class BoolContent : AStringContent
{
    /** Create a content with _symbol name symbol. */
    this (char[] symbol) {
        auto valp = symbol in changed.boolData;
        if (valp)
            v = *valp;
        sv = v ? "true" : "false";
	super (symbol);
    }
    
    // Assign without adding change to save changeset
    void assignNoCng (bool val) {
	v = val;
	sv = v ? "true" : "false";
	if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    void opAssign (bool val) {
	assignNoCng (val);
        endCng;
    }
    bool opCall () {
        return v;
    }
    alias opCall opCast;
    
    override bool endEdit () {
	try {
	    sv = Util.trim (Ascii.toLower (sv));	// NOTE: sv must be in mutable memory
	    if (sv == "false")
		v = 0;
	    else if (sv == "true")
		v = 1;
	    else 	// throws if can't convert to int:
		v = (Int.toLong (sv) != 0);
	} catch (Exception e) {
	    logger.error (e.msg);
	    sv = v ? "true" : "false";
	    return false;
	}
	sv = v ? "true" : "false";
        endEvent;
        endCng;
	return true;
    }
    
    // Add change to changeset
    void endCng () {
        changed.boolData[symbol] = v;
    }
    
protected:
    bool v;
}

/** Text content. */
class StringContent : AStringContent
{
    this (char[] symbol) {
        auto valp = symbol in changed.charAData;
        if (valp)
            v = *valp;
        super (symbol);
    }
    
    void assignNoCng (char[] val) {
	v = val.dup;
	if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    void opAssign (char[] val) {
	assignNoCng (val);
        endCng;
    }
    char[] opCall () {
        return v;
    }
    alias opCall opCast;
    
    override bool endEdit () {
	endEvent;
        endCng;
	return true;
    }
    
    void endCng () {
        changed.charAData[symbol] = v;
    }
    
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) {
	super (symbol);
	auto valp = symbol in changed.intData;
        if (valp)
            v = *valp;
        sv = Int.format (svBuf, v);
    }
    
    override bool set (IContent c) {
	UnionContent uc;
	uc.bc = cast (BoolContent) c;
	if (uc.bc !is null) {
	    this = uc.bc();
	    return true;
	}
	uc.ic = cast (IntContent) c;
	if (uc.ic !is null) {
	    this = uc.ic();
	    return true;
	}
	uc.dc = cast (DoubleContent) c;
	if (uc.dc !is null) {
	    this = Math.rndint (uc.dc());	// round to nearest
	    return true;
	}
	return super.set (c);
    }
    
    void assignNoCng (int val) {
	v = val;
	sv = Int.format (svBuf, v);
	if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    void opAssign (int val) {
	assignNoCng (val);
	endCng;
    }
    int opCall () {
        return v;
    }
    alias opCall opCast;
    
    override bool endEdit () {
	try {
	    // use toFloat to allow decimal points and exponents
            v = Math.rndint (Float.toFloat (sv));
        } catch (Exception e) {
            logger.error (e.msg);
	    sv = Int.format (svBuf, v);
	    return false;
	}
        sv = Int.format (svBuf, v);
        endEvent;
        endCng;
	return true;
    }
    
    void endCng () {
        changed.intData[symbol] = v;
    }
    
protected:
    int v;
}

/** Floating-point content. */
class DoubleContent : AStringContent
{
    /** Create a content with _symbol name symbol. */
    this (char[] symbol) {
	super (symbol);
	auto valp = symbol in changed.doubleData;
        if (valp)
            v = *valp;
        sv = Float.format (svBuf, v, 8, 4);
    }
    
    override bool set (IContent c) {
	UnionContent uc;
	uc.bc = cast (BoolContent) c;
	if (uc.bc !is null) {
	    this = uc.bc();
	    return true;
	}
	uc.ic = cast (IntContent) c;
	if (uc.ic !is null) {
	    this = uc.ic();
	    return true;
	}
	uc.dc = cast (DoubleContent) c;
	if (uc.dc !is null) {
	    this = uc.dc();
	    return true;
	}
	return super.set (c);
    }
    
    void assignNoCng (double val) {
	v = val;
	sv = Float.format (svBuf, v, 8, 4);
	if (pos > sv.length) pos = sv.length;
        endEvent;
    }
    void opAssign (double val) {
	assignNoCng (val);
        endCng;
    }
    double opCall () {
        return v;
    }
    alias opCall opCast;
    
    override bool endEdit () {
        try {
            v = Float.toFloat (sv);
        } catch (Exception e) {
            logger.error (e.msg);
	    sv = Float.format (svBuf, v, 8, 4);
	    return false;
	}
        sv = Float.format (svBuf, v, 8, 4);
        endEvent;
        endCng;
	return true;
    }
    
    void endCng () {
        changed.doubleData[symbol] = v;
    }
    
protected:
    double v;
}

/** A content representing an enumeration. */
class EnumContent : AStringContent, IContentList
{
    /** CTOR.
    *
    * Params:
    *	enumSymbols = Symbol names for each
    *	val = which value is active; must be in [0,length-1]
    */
    this (char[] symbol, char[][] enumSymbols) {
        super (symbol);
        this.enumSymbols = enumSymbols;
        enums.length = enumSymbols.length;
        char[] symPeriod = symbol~'.';
        foreach (i, ref e; enums) {
            e = new EnumValueContent (this, i, symPeriod~enumSymbols[i]);
        }
        enums[v].assignFromParent (true);
	sv = enums[v].name_;
        // Re-set the value if a saved value is found:
        auto valp = symbol in changed.enumValData;
        if (valp)
            assignNoCng = *valp;
    }
    
    void opAssign (size_t val) {
        assignNoCng (val);
        endCng;
    }
    // Assign by enum symbol name (for ContentLoader)
    void assignNoCng (char[] enumSym) {
        foreach (i,e; enumSymbols) {
            if (e == enumSym) {
                assignNoCng (i);
                return;
            }
        }
        logger.error ("EnumContent {} assigned invalid enumeration: {}; valid: {}", symbol, enumSym, enumSymbols);
    }
    size_t opCall () {
        //debug logger.trace ("EnumContent {} returning value: {} ({})",symbol, enumSymbols[v], v);
        return v;
    }
    alias opCall opCast;
    void assignNoCng (size_t val) {
        if (val >= enums.length) {
	    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;
    }
    
    override bool endEdit () {
	foreach (i,e; enums)
	    if (sv == e.name_) {
		assignNoCng (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;
	return false;
	
	break1:
	endCng;
	return true;
    }
    
    void endCng () {
        changed.enumValData[symbol] = enumSymbols[v];
    }

    override Content[] list () {
        return enums;
    }
    
    // Interface functions that don't make sense for an emuneration:
    override void append (Content) {}
    
protected:
    // Called by child; change this if true, assert current value if false
    void childAssign (size_t val) {
        debug assert (val < enums.length, "cA out of bounds");
        if (enums[val].v)
            this = val;
        else
            enums[val].assignFromParent (v == val);
    }
    
    size_t v;			// value (i.e. enums[v] is value)
    EnumValueContent[] enums;
    char[][] enumSymbols;	// saved for getStructOf
    
    /** Special version of BoolContent for each enumeration to update the
     * parent Enum.
     * 
     * Also should not save its value, since the parent stores the value. */
    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);
        }
        
        override void assignNoCng (bool val) {
            v = val;
            parent.childAssign (i);
        }
        override void opAssign (bool val) {
            assignNoCng (val);
        }
        void assignFromParent (bool val) {	// don't call back to parent
            super.assignNoCng (val);
        }
        
        override bool endEdit () {
	    if (super.endEdit) {
		parent.childAssign (i);
		return true;	// value accepted by BoolContent, not necessarily by EnumContent
	    }
            return false;
        }
    
    protected:
        EnumContent parent;
        size_t i;
    }
}

debug (mdeUnitTest) {
    unittest {
	bool throws (void delegate() dg) {
	    bool r = false;
	    try {
		dg();
	    } catch (Exception e) {
		r = true;
	    }
	    return r;
	}
	
	StringContent sc = new StringContent ("unittest.sc");
	IntContent ic = new IntContent ("unittest.ic");
	BoolContent bc = new BoolContent ("unittest.bc");
	DoubleContent dc = new DoubleContent ("unittest.dc");
	
	logger.info ("You should see some \"invalid literal\" errors:");
	sc = "16";
	ic.set = sc;
	assert (ic() == 16);
	sc = "five";	// fails
	ic.set = sc;
	assert (ic.toString(0) == "16");
	
	bc.set = ic;
	assert (bc());
	sc = "fALse";
	bc.set = sc;
	assert (!bc());
	
	sc = "2.5";
	ic.set = sc;	// parses as float and rounds
	assert (ic() == 2);	// rounds to even
	sc = "31.5";
	dc.set = sc;
	ic.set = dc;	// rounds to even
	assert (ic() == 32);
	dc = -1.5;
	ic.set = dc;	// rounds to even
	assert (ic() == -2);
	
	bc.set = dc;	// fails: not included conversion
	assert (!bc());
	bc.set = ic;
	assert (bc());
	
	logger.info ("Unittest complete.");
    }
}