#
# Copyright (c) 2002, 2003, 2004, 2005 Art Haas
#
# This file is part of PythonCAD.
#
# PythonCAD 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.
#
# PythonCAD 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 PythonCAD; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# basic text functionality
#

import math
import types

from PythonCAD.Generic import color
from PythonCAD.Generic import entity
from PythonCAD.Generic import util

def font_style_string(style):
    """Return a text string for the font style.

font_style_string(style)
    """
    if style == TextStyle.FONT_NORMAL:
        _str = 'normal'
    elif style == TextStyle.FONT_OBLIQUE:
        _str = 'oblique'
    elif style == TextStyle.FONT_ITALIC:
        _str = 'italic'
    else:
        raise ValueError, "Unknown font style: " + str(style)
    return _str

def font_weight_string(weight):
    """Return a text string for the font weight.

font_weight_string(weight)
    """
    if weight == TextStyle.WEIGHT_NORMAL:
        _str = 'normal'
    elif weight == TextStyle.WEIGHT_LIGHT:
        _str = 'light'
    elif weight == TextStyle.WEIGHT_BOLD:
        _str = 'bold'
    elif weight == TextStyle.WEIGHT_HEAVY:
        _str = 'heavy'
    else:
        raise ValueError, "Unknown font weight: " + str(weight)
    return _str

#
# the font_prop_map and parse_font() should be moved as
# they are GTK specific ...
#

font_prop_map = {
    'Oblique' : 'style',
    'Italic' : 'style',
    'Ultra-Light' : 'weight',
    'Light' : 'weight',
    'Medium' : 'weight',
    'Semi-Bold' : 'weight',
    'Bold' : 'weight',
    'Ultra-Bold' : 'weight',
    'Heavy' : 'weight',
    'Ultra-Condensed' : 'stretch',
    'Extra-Condensed' : 'stretch',
    'Condensed' : 'stretch',
    'Semi-Condensed' : 'stretch',
    'Semi-Expanded' : 'stretch',
    'Expanded' : 'stretch',
    'Extra-Expanded' : 'stretch',
    'Ultra-Expanded' : 'stretch',
    }

def parse_font(fontstr):
    _size = 12
    _weight = 0 # NORMAL
    _style = 0 # NORMAL
    _stretch = 0# NORMAL
    _family = 'Sans'
    if fontstr != '':
        _fontlist = fontstr.split()
        _fontlist.reverse()
        if _fontlist[0].isdigit():
            _sz = _fontlist.pop(0)
            _size = int(_sz)
        while (_fontlist[0] in font_prop_map):
            _prop = _fontlist.pop(0)
            _item = font_prop_map[_prop]
            # print "prop: " + _prop
            # print "item: " + _item
            if _item == 'style':
                if _prop == 'Oblique':
                    _style = 1
                elif _prop == 'Italic':
                    _style = 2
                else:
                    _style = 0 # NORMAL # default
            elif _item == 'weight':
                if (_prop == 'Ultra-Light' or
                    _prop == 'Light' or
                    _prop == 'Medium'):
                    _weight = 1
                elif (_prop == 'Semi-Bold' or
                      _prop == 'Bold'):
                    _weight = 2
                elif (_prop == 'Ultra-Bold' or
                      _prop == 'Heavy'):
                    _weight = 3
                else:
                    _weight = 0 # NORMAL
            elif _item == 'stretch':
                _stretch = _prop # fixme - add stretching bits
            else:
                raise ValueError, "Unknown font property: " + _item
        _fontlist.reverse()
        if len(_fontlist):
            _family = ' '.join(_fontlist)
    return (_family, _style, _weight, _stretch, _size)

#
# Style class for a text block
#

class TextStyle(object):
    """A class for describing text properties.

A TextStyle object has the following attributes:

family: The font family
style: The font style
weight: The font weight
color: The font color
alignment: Text positioning relative to the location
angle: Angular position of the text

A TextStyle object has the following methods:

getFamily(): Get the font family.
getStyle(): Get the font style.
getWeight(): Get the font weight.
getColor(): Get the font color.
getSize(): Get the text size.
getAngle(): Get the text angle
getAlignment(): Get the text positioning
    """

    FONT_NORMAL = 0
    FONT_OBLIQUE = 1
    FONT_ITALIC = 2

    WEIGHT_NORMAL = 0
    WEIGHT_LIGHT = 1
    WEIGHT_BOLD = 2
    WEIGHT_HEAVY = 3

    ALIGN_LEFT = 0
    ALIGN_CENTER = 1
    ALIGN_RIGHT = 2

    __defcolor = color.Color(0xffffff)

    def __init__(self, name, **kw):
        """Initialize a TextStyle object.

ts = TextStyle(name)

The following are the defaults:

family: Sans
style: NORMAL
weight: NORMAL
color: White (#ffffff)
size: 1.0
angle: 0.0
alignment: LEFT
        """
        _name = name
        if not isinstance(_name, unicode):
            _name = unicode(name)
        _family = 'Sans'
        if 'family' in kw:
            _family = kw['family']
            if not isinstance(_family, str):
                raise TypeError, "Invalid font family: " + str(_family)
        _style = TextStyle.FONT_NORMAL
        if 'style' in kw:
            _style = kw['style']
            if not isinstance(_style, int):
                raise TypeError, "Invalid font style: " + str(_style)
            if (_style != TextStyle.FONT_NORMAL and
                _style != TextStyle.FONT_OBLIQUE and
                _style != TextStyle.FONT_ITALIC):
                raise ValueError, "Invalid font style value: %d" % _style
        _weight = TextStyle.WEIGHT_NORMAL
        if 'weight' in kw:
            _weight = kw['weight']
            if not isinstance(_weight, int):
                raise TypeError, "Invalid font weight: " + str(_weight)
            if (_weight != TextStyle.WEIGHT_NORMAL and
                _weight != TextStyle.WEIGHT_LIGHT and
                _weight != TextStyle.WEIGHT_BOLD and
                _weight != TextStyle.WEIGHT_HEAVY):
                raise ValueError, "Invalid font weight value: %d" % _weight
        _color = TextStyle.__defcolor
        if 'color' in kw:
            _color = kw['color']
            if not isinstance(_color, color.Color):
                raise TypeError, "Invalid color: " + str(_color)
        _size = 1.0
        if 'size' in kw:
            _size = util.get_float(kw['size'])
            if _size < 0.0:
                raise ValueError, "Invalid text size: %g" % _size
        _angle = 0.0
        if 'angle' in kw:
            _angle = util.get_float(kw['angle'])
            if _angle > 360.0 or _angle < -360.0:
                _angle = math.fmod(_angle, 360.0)
        _align = TextStyle.ALIGN_LEFT
        if 'align' in kw:
            _align = kw['align']
            if not isinstance(_align, int):
                raise TypeError, "Invalid text alignment: " + str(_align)
            if (_align != TextStyle.ALIGN_LEFT and
                _align != TextStyle.ALIGN_CENTER and
                _align != TextStyle.ALIGN_RIGHT):
                raise ValueError, "Invalid text alignment value: %d" % _align
        super(TextStyle, self).__init__()
        self.__name = _name
        self.__family = _family
        self.__style = _style
        self.__weight = _weight
        self.__color = _color
        self.__size = _size
        self.__angle = _angle
        self.__alignment = _align

    def __eq__(self, obj):
        if not isinstance(obj, TextStyle):
            return False
        return (self.__name == obj.getName() and
                self.__family == obj.getFamily() and
                self.__style == obj.getStyle() and
                self.__weight == obj.getWeight() and
                self.__color == obj.getColor() and
                abs(self.__size - obj.getSize()) < 1e-10 and
                abs(self.__angle - obj.getAngle()) < 1e-10 and
                self.__alignment == obj.getAlignment())

    def __ne__(self, obj):
        if not isinstance(obj, TextStyle):
            return True
        return (self.__name != obj.getName() or
                self.__family != obj.getFamily() or
                self.__style != obj.getStyle() or
                self.__weight != obj.getWeight() or
                self.__color != obj.getColor() or
                abs(self.__size - obj.getSize()) > 1e-10 or
                abs(self.__angle - obj.getAngle()) > 1e-10 or
                self.__alignment != obj.getAlignment())

    def finish(self):
        """Finalization for TextStyle instances.
        """
        self.__color = None


    def getValues(self):
        _vals = {}
        _vals['name'] = self.__name
        _vals['family'] = self.__family
        _vals['style'] = self.__style
        _vals['weight'] = self.__weight
        _vals['color'] = self.__color.getColors()
        _vals['size'] = self.__size
        _vals['angle'] = self.__angle
        _vals['align'] = self.__alignment
        return _vals

    def getName(self):
        """Retrieve the name of the TextStyle.

getName()
        """
        return self.__name

    name = property(getName, None, None, "TextStyle name.")

    def getFamily(self):
        """Return the font family.

getFamily()
        """
        return self.__family

    family = property(getFamily, None, None, "Text font family")

    def getStyle(self):
        """Return the font style.

getStyle()
        """
        return self.__style

    style = property(getStyle, None, None, "Text font style")

    def getWeight(self):
        """Return the font weight.

getWeight()
        """
        return self.__weight

    weight = property(getWeight, None, None, "Text font weight")

    def getColor(self):
        """Return the font color.

getColor()
        """
        return self.__color

    color = property(getColor, None, None, "Text color")

    def getSize(self):
        """Return the specified size.

getSize()
        """
        return self.__size

    size = property(getSize, None, None, "Text size.")

    def getAngle(self):
        """Return the angle at which the text is drawn.

getAngle()

This method returns an angle -360.0 < angle < 360.0.
        """
        return self.__angle

    angle = property(getAngle, None, None, "Text angle.")

    def getAlignment(self):
        """Return the line justification setting.

getAlignment()
        """
        return self.__alignment

    alignment = property(getAlignment, None, "Text alignment.")

#
# TextBlock
#

class TextBlock(entity.Entity):
    """A class representing text in a drawing.

A TextBlock instance has the following attributes:

text: Text within the TextBlock
location: Spatial location of the TextBlock
family: The font family
style: The font style
weight: The font weight
color: The font color
size: Text size
alignment: Text positioning at the location
angle: Angular position of the text

A TextBlock instance has the following methods:

{get/set}TextStyle(): Get/Set the TextStyle for the TextBlock
{get/set}Text(): Get/Set the text
{get/set}Location(): Get/Set the TextBlock location
{get/set}Family(): Get/Set the font family.
{get/set}Style(): Get/Set the font style.
{get/set}Weight(): Get/Set the font weight.
{get/set}Color(): Get/Set the font color.
{get/set}Size(): Get/Set the text size.
{get/set}Angle(): Get/Set the text angle
{get/set}Alignment(): Get/Set the text positioning
{get/set}Bounds(): Get/Set the height and width of the TextBlock.
{get/set}FontScale(): Get/Set a scale factor used for font display.
getLineCount(): Get the number of lines stored in the TextBlock
    """
    __messages = {
        'text_changed' : True,
        'textstyle_changed' : True,
        'font_family_changed' : True,
        'font_style_changed' : True,
        'font_weight_changed' : True,
        'font_color_changed' : True,
        'text_size_changed' : True,
        'text_angle_changed' : True,
        'text_alignment_changed' : True,
        'moved' : True,
        }

    __defcolor = color.Color(0xffffff)

    __defstyle = TextStyle(u'Default Text Style', color=__defcolor)

    def __init__(self, x, y, text, textstyle=None, **kw):
        """Initialize a TextBlock instance.

TextBlock(x, y, text[, textstyle=None])
        """
        _x = util.get_float(x)
        _y = util.get_float(y)
        if not isinstance(text, types.StringTypes):
            raise TypeError, "Invalid text data: " + str(text)
        _tstyle = textstyle
        if _tstyle is None:
            _tstyle = TextBlock.__defstyle
        else:
            if not isinstance(_tstyle, TextStyle):
                raise TypeError, "Invalid TextStyle object: " + `_tstyle`
        _family = None
        if 'family' in kw:
            _family = kw['family']
            if not isinstance(_family, types.StringTypes):
                raise TypeError, "Invalid font family: " + str(_family)
            if _family == _tstyle.getFamily():
                _family = None
        _style = None
        if 'style' in kw:
            _style = kw['style']
            if not isinstance(_style, int):
                raise TypeError, "Invalid font style: " + str(_style)
            if (_style != TextStyle.FONT_NORMAL and
                _style != TextStyle.FONT_OBLIQUE and
                _style != TextStyle.FONT_ITALIC):
                raise ValueError, "Invalid font style value: %d" % _style
            if _style == _tstyle.getStyle():
                _style = None
        _weight = None
        if 'weight' in kw:
            _weight = kw['weight']
            if not isinstance(_weight, int):
                raise TypeError, "Invalid font weight: " + str(_weight)
            if (_weight != TextStyle.WEIGHT_NORMAL and
                _weight != TextStyle.WEIGHT_LIGHT and
                _weight != TextStyle.WEIGHT_BOLD and
                _weight != TextStyle.WEIGHT_HEAVY):
                raise ValueError, "Invalid font weight value: %d" % _weight
            if _weight == _tstyle.getWeight():
                _weight = None
        _color = None
        if 'color' in kw:
            _color = kw['color']
            if not isinstance(_color, color.Color):
                raise TypeError, "Invalid font color: " + str(_color)
            if _color == _tstyle.getColor():
                _color = None
        _size = None
        if 'size' in kw:
            _size = util.get_float(kw['size'])
            if _size < 0.0:
                raise ValueError, "Invalid text size: %g" % _size
            if abs(_size - _tstyle.getSize()) < 1e-10:
                _size = None
        _angle = None
        if 'angle' in kw:
            _angle = util.get_float(kw['angle'])
            if _angle > 360.0 or _angle < -360.0:
                _angle = math.fmod(_angle, 360.0)
            if abs(_angle - _tstyle.getAngle()) < 1e-10:
                _angle = None
        _align = None
        if 'align' in kw:
            _align = kw['align']
            if not isinstance(_align, int):
                raise TypeError, "Invalid text alignment: " + str(_align)
            if (_align != TextStyle.ALIGN_LEFT and
                _align != TextStyle.ALIGN_CENTER and
                _align != TextStyle.ALIGN_RIGHT):
                raise ValueError, "Invalid text alignment value: %d" % _align
            if _align == _tstyle.getAlignment():
                _align = None
        super(TextBlock, self).__init__(**kw)
        self.__location = (_x, _y)
        self.__text = text
        self.__tstyle = _tstyle
        self.__family = _family
        self.__style = _style
        self.__weight = _weight
        self.__color = _color
        self.__size = _size
        self.__angle = _angle
        self.__alignment = _align
        self.__bounds = None
        self.__scale = None

    def getDefaultTextStyle(cls):
        return cls.__defstyle

    getDefaultTextStyle = classmethod(getDefaultTextStyle)

    def finish(self):
        """Finalization for TextBlock instances.

finish()
        """
        if self.__color is not None:
            self.__color = None
        super(TextBlock, self).finish()

    def getValues(self):
        """Return values comprising the TextBlock.

getValues()

This method extends the Entity::getValues() method.
        """
        _data = super(TextBlock, self).getValues()
        _data.setValue('type', 'textblock')
        _data.setValue('location', self.__location)
        _data.setValue('text', self.__text)
        if self.__tstyle is TextBlock.__defstyle:
            _data.setValue('textstyle', None)
        else:
            _data.setValue('textstyle', self.__tstyle.getValues())
        if self.__family is not None:
            _data.setValue('family', self.__family)
        if self.__style is not None:
            _data.setValue('style', self.__style)
        if self.__weight is not None:
            _data.setValue('weight', self.__weight)
        if self.__color is not None:
            _data.setValue('color', self.__color.getColors())
        if self.__size is not None:
            _data.setValue('size', self.__size)
        if self.__angle is not None:
            _data.setValue('angle', self.__angle)
        if self.__alignment is not None:
            _data.setValue('align', self.__alignment)
        return _data

    def getText(self):
        """Get the current text within the TextBlock.

getText()
        """
        return self.__text

    def setText(self, text):
        """Set the text within the TextBlock.

setText(text)
        """
        if not isinstance(text, types.StringTypes):
            raise TypeError, "Invalid text data: " + str(text)
        _ot = self.__text
        if _ot != text:
            self.startChange('text_changed')
            self.__text = text
            self.endChange('text_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('text_changed', _ot)
            self.modified()

    text = property(getText, setText, None, "TextBlock text.")

    def getLocation(self):
        """Return the TextBlock spatial position.

getLocation()
        """
        return self.__location

    def setLocation(self, x, y):
        """Store the spatial position of the TextBlock.

setLocation(x, y)

Arguments 'x' and 'y' should be float values.
        """
        _x = util.get_float(x)
        _y = util.get_float(y)
        _ox, _oy = self.__location
        if abs(_ox - _x) > 1e-10 or abs(_oy - _y) > 1e-10:
            self.startChange('moved')
            self.__location = (_x, _y)
            self.endChange('moved')
            self.sendMessage('moved', _ox, _oy)
            self.modified()

    location = property(getLocation, None, None, "TextBlock location")

    def getTextStyle(self):
        """Return the TextStyle associated with this TextBlock.

getTextStyle()
        """
        return self.__tstyle

    def setTextStyle(self, textstyle=None):
        """Store the TextStyle associated with this TextBlock.

setTextStyle([textstyle])

Optional argument 'textstyle' should be an TextStyle instance. If
no argument is given the TextBlock will use a default TextStyle.
The TextBlock will have all the text appearance and positioning
attributes set the the values in the TextStyle.
        """
        _ts = textstyle
        if _ts is None:
            _ts = TextBlock.__defstyle
        if not isinstance(_ts, TextStyle):
            raise TypeError, "Invalid text style: " + `_ts`
        _os = self.__tstyle
        if _os != _ts:
            _opts = {}
            if self.__family is not None:
                _opts['family'] = self.__family
            if self.__style is not None:
                _opts['style'] = self.__style
            if self.__weight is not None:
                _opts['weight'] = self.__weight
            if self.__color is not None:
                _opts['color'] = self.__color
            if self.__size is not None:
                _opts['size'] = self.__size
            if self.__angle is not None:
                _opts['angle'] = self.__angle
            if self.__alignment is not None:
                _opts['align'] = self.__alignment
            self.startChange('textstyle_changed')
            self.__tstyle = _ts
            self.endChange('textstyle_changed')
            #
            # call the methods with no arguments to set the values
            # given in the new TextStyle
            #
            self.setFamily()
            self.setStyle()
            self.setWeight()
            self.setColor()
            self.setSize()
            self.setAngle()
            self.setAlignment()
            self.sendMessage('textstyle_changed', _os, _opts)
            self.modified()

    def getFamily(self):
        """Return the font family.

getFamily()
        """
        _family = self.__family
        if _family is None:
            _family = self.__tstyle.getFamily()
        return _family

    def setFamily(self, family=None):
        """Set the font family.

setFamily([family])

Optional argument 'family' should be a string giving the
font family. Calling this method without an argument
sets the font family to that defined in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting family not allowed - object locked."
        _family = family
        if _family is not None:
            if not isinstance(family, types.StringTypes):
                raise TypeError, "Invalid family type: " + `type(family)`
        _f = self.getFamily()
        if ((_family is None and self.__family is not None) or
            (_family is not None and _family != _f)):
            self.startChange('font_family_changed')
            self.__family = _family
            self.endChange('font_family_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('font_family_changed', _f)
            self.modified()

    family = property(getFamily, setFamily, None, "Text object font family")

    def getStyle(self):
        """Return the font style.

getStyle()
        """
        _style = self.__style
        if _style is None:
            _style = self.__tstyle.getStyle()
        return _style

    def setStyle(self, style=None):
        """Set the font style.

setStyle([style])

Optional argument 'style' should be one of the following:

TextStyle.FONT_NORMAL
TextStyle.FONT_OBLIQUE
TextStyle.FONT_ITALIC

Calling this method without an argument restores the font
style to that defined in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting style not allowed - object locked."
        _style = style
        if _style is not None:
            if not isinstance(_style, int):
                raise TypeError, "Invalid TextStyle font style type: " + `type(_style)`
            if (_style != TextStyle.FONT_NORMAL and
                _style != TextStyle.FONT_OBLIQUE and
                _style != TextStyle.FONT_ITALIC):
                raise ValueError, "Invalid font style: " + str(_style)
        _s = self.getStyle()
        if ((_style is None and self.__style is not None) or
            (_style is not None and _style != _s)):
            self.startChange('font_style_changed')
            self.__style = _style
            self.endChange('font_style_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('font_style_changed', _s)
            self.modified()

    style = property(getStyle, setStyle, None, "Text font style")

    def getWeight(self):
        """Return the font weight.

getWeight()
        """
        _weight = self.__weight
        if _weight is None:
            _weight = self.__tstyle.getWeight()
        return _weight

    def setWeight(self, weight=None):
        """Set the font weight.

setWeight([weight])

Optional argument 'weight' should be one of the following values:

TextStyle.WEIGHT_NORMAL
TextStyle.WEIGHT_LIGHT
TextStyle.WEIGHT_BOLD
TextStyle.WEIGHT_HEAVY

Calling this method without an argument restores the font weight
to that defined in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting weight not allowed - object locked."
        _weight = weight
        if _weight is not None:
            if not isinstance(_weight, int):
                raise TypeError, "Invalid TextStyle font weight type: " + `type(_weight)`
            if (_weight != TextStyle.WEIGHT_NORMAL and
                _weight != TextStyle.WEIGHT_LIGHT and
                _weight != TextStyle.WEIGHT_BOLD and
                _weight != TextStyle.WEIGHT_HEAVY):
                raise ValueError, "Invalid text weight: %d" % _weight
        _w = self.getWeight()
        if ((_weight is None and self.__weight is not None) or
            (_weight is not None and _weight != _w)):
            self.startChange('font_weight_changed')
            self.__weight = _weight
            self.endChange('font_weight_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('font_weight_changed', _w)
            self.modified()

    weight = property(getWeight, setWeight, None, "Text font weight")

    def getColor(self):
        """Return the font color.

getColor()
        """
        _color = self.__color
        if _color is None:
            _color = self.__tstyle.getColor()
        return _color

    def setColor(self, col=None):
        """Set the font color.

setColor([col])

Optional argument 'col' should be a Color object. Calling
this method without an argument restores the font color to
that defined in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting color not allowed - object locked."
        _col = col
        if _col is not None:
            if not isinstance(_col, color.Color):
                raise TypeError, "Invalid color type: " + `type(_col)`
        _c = self.getColor()
        if ((_col is None and self.__color is not None) or
            (_col is not None and _col != _c)): 
            self.startChange('font_color_changed')
            self.__color = _col
            self.endChange('font_color_changed')
            self.sendMessage('font_color_changed', _c)
            self.modified()

    color = property(getColor, setColor, None, "Text color")

    def getSize(self):
        """Return the text size.

getSize()
        """
        _size = self.__size
        if _size is None:
            _size = self.__tstyle.getSize()
        return _size

    def setSize(self, size=None):
        """Set the size of the text.

setSize([size])

Optionala rgument 'size' should be a float value greater than 0.
Calling this method without an argument restores the text size
to the value given in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting size not allowed - object locked."
        _size = size
        if _size is not None:
            _size = util.get_float(_size)
            if _size < 0.0:
                raise ValueError, "Invalid size: %g" % _size
        _os = self.getSize()
        if ((_size is None and self.__size is not None) or
            (_size is not None and abs(_size - _os) > 1e-10)):
            self.startChange('text_size_changed')
            self.__size = _size
            self.endChange('text_size_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('text_size_changed', _os)
            self.modified()

    size = property(getSize, setSize, None, "Text size.")

    def getAngle(self):
        """Return the angle at which the text is drawn.

getAngle()

This method returns an angle -360.0 < angle < 360.0.
        """
        _angle = self.__angle
        if _angle is None:
            _angle = self.__tstyle.getAngle()
        return _angle

    def setAngle(self, angle=None):
        """Set the angle at which the text block should be drawn.

setAngle([angle])

Optional argument 'angle' should be a float value. Calling this
method without arguments sets the angle to be the value defined
in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting angle not allowed - object locked."
        _angle = angle
        if _angle is not None:
            _angle = util.get_float(_angle)
            if _angle > 360.0 or _angle < -360.0:
                _angle = math.fmod(_angle, 360.0)
        _a = self.getAngle()
        if ((_angle is None and self.__angle is not None) or
            (_angle is not None and abs(_angle - _a) > 1e-10)):
            self.startChange('text_angle_changed')
            self.__angle = _angle
            self.endChange('text_angle_changed')
            self.sendMessage('text_angle_changed', _a)
            self.modified()

    angle = property(getAngle, setAngle, None, "Text angle.")

    def getAlignment(self):
        """Return the line justification setting.

getAlignment()
        """
        _align = self.__alignment
        if _align is None:
            _align = self.__tstyle.getAlignment()
        return _align

    def setAlignment(self, align=None):
        """Set left, center, or right line justification.

setAlignment([align])

Optional argument 'align' should be one of

TextStyle.ALIGN_LEFT
TextStyle.ALIGN_CENTER
TextStyle.ALIGN_RIGHT

Calling this method without arguments sets the text alignment
to be that given in the TextStyle.
        """
        if self.isLocked():
            raise RuntimeError, "Setting alignment not allowed - object locked."
        _align = align
        if _align is not None:
            if not isinstance(_align, int):
                raise TypeError, "Invalid TextStyle alignment type: " + `type(_align)`
            if (_align != TextStyle.ALIGN_LEFT and
                _align != TextStyle.ALIGN_CENTER and
                _align != TextStyle.ALIGN_RIGHT):
                raise ValueError, "Invalid text alignment value: %d" % _align
        _a = self.getAlignment()
        if ((_align is None and self.__alignment is not None) or
            (_align is not None and _align != _a)):
            self.startChange('text_alignment_changed')
            self.__alignment = _align
            self.endChange('text_alignment_changed')
            if self.__bounds is not None:
                self.__bounds = None
            self.sendMessage('text_alignment_changed', _a)
            self.modified()

    alignment = property(getAlignment, setAlignment, None, "Text alignment.")

    def move(self, dx, dy):
        """Move a TextBlock.

move(dx, dy)

The first argument gives the x-coordinate displacement,
and the second gives the y-coordinate displacement. Both
values should be floats.
        """
        if self.isLocked():
            raise RuntimeError, "Moving not allowed - object locked."
        _dx = util.get_float(dx)
        _dy = util.get_float(dy)
        if abs(_dx) > 1e-10 or abs(_dy) > 1e-10:
            _x, _y = self.__location
            self.startChange('moved')
            self.__location = ((_x + _dx), (_y + _dy))
            self.endChange('moved')
            self.sendMessage('moved', _x, _y)
            self.modified()

    def getLineCount(self):
        """Return the number of lines of text in the TextBlock

getLineCount()        
        """
        #
        # ideally Python itself would provide a linecount() method
        # so the temporary list would not need to be created ...
        #
        return len(self.__text.splitlines())

    def clone(self):
        """Return an identical copy of a TextBlock.

clone()        
        """
        _x, _y = self.getLocation()
        _text = self.getText()
        _textstyle = self.getTextStyle()
        _tb = TextBlock(_x, _y, _text, _textstyle)
        _family = self.getFamily()
        if _family != _textstyle.getFamily():
            _tb.setFamily(_family)
        _style = self.getStyle()
        if _style != _textstyle.getStyle():
            _tb.setStyle(_style)
        _weight = self.getWeight()
        if _weight != _textstyle.getWeight():
            _tb.setWeight(_weight)
        _color = self.getColor()
        if _color != _textstyle.getColor():
            _tb.setColor(_color)
        _size = self.getSize()
        if abs(_size - _textstyle.getSize()) > 1e-10:
            _tb.setSize(_size)
        _angle = self.getAngle()
        if abs(_angle - _textstyle.getAngle()) > 1e-10:
            _tb.setAngle(_angle)
        _align = self.getAlignment()
        if _align != _textstyle.getAlignment():
            _tb.setAlignment(_align)
        return _tb
        
    def getBounds(self):
        """Get the width and height of the TextBlock.

getBounds()

This method can return None if the boundary has not been calculated
or the TextBlock has been changed.
        """
        return self.__bounds

    def setBounds(self, width=None, height=None):
        """Set the width and height of the TextBlock.

setBounds([width, height])

Arguments 'width' and 'height' should be positive float values
if used. If both arguments are None, the boundary of the TextBlock
is unset.
        """
        _w = width
        _h = height
        if _w is None and _h is None:
            self.__bounds = None
        else:
            if _w is None:
                raise ValueError, "Width cannot be None"
            _w = util.get_float(_w)
            if _w < 0.0:
                raise ValueError, "Invalid width: %g" % _w
            if _h is None:
                raise ValueError, "Height cannot be None"
            _h = util.get_float(_h)
            if _h < 0.0:
                raise ValueError, "Invalid height: %g" % _w
            self.__bounds = (_w, _h)

    def getFontScale(self):
        """Return the stored font scaling factor

getFontScale()

This method will raise a ValueError exception if there has
not be a value stored with the setFontScale() call.
        """
        if self.__scale is None:
            raise ValueError, "Scale not set."
        return self.__scale

    def setFontScale(self, scale):
        """Store a value used to scale the TextBlock font.

setFontScale(scale)

Argument 'scale' should be a positive float value.
        """
        _s = util.get_float(scale)
        if not _s > 0.0:
            raise ValueError, "Invalid scale value: %g" % _s
        self.__scale = _s

    def sendsMessage(self, m):
        if m in TextBlock.__messages:
            return True
        return super(TextBlock, self).sendsMessage(m)

#
# TextBlock history class
#

class TextBlockLog(entity.EntityLog):
    __setops = {
        'text_changed' : TextBlock.setText,
        'font_family_changed' : TextBlock.setFamily,
        'font_style_changed' : TextBlock.setStyle,
        'font_color_changed' : TextBlock.setColor,
        'font_weight_changed' : TextBlock.setWeight,
        'text_size_changed' : TextBlock.setSize,
        'text_angle_changed' : TextBlock.setAngle,
        'text_alignment_changed' : TextBlock.setAlignment,
        }

    __getops = {
        'text_changed' : TextBlock.getText,
        'font_family_changed' : TextBlock.getFamily,
        'font_style_changed' : TextBlock.getStyle,
        'font_color_changed' : TextBlock.getColor,
        'font_weight_changed' : TextBlock.getWeight,
        'text_size_changed' : TextBlock.getSize,
        'text_angle_changed' : TextBlock.getAngle,
        'text_alignment_changed' : TextBlock.getAlignment,
        }

    def __init__(self, tblock):
        if not isinstance(tblock, TextBlock):
            raise TypeError, "Invalid TextBlock: " + `tblock`
        super(TextBlockLog, self).__init__(tblock)
        tblock.connect('textstyle_changed', self._textstyleChanged)
        tblock.connect('text_changed', self._textChanged)
        tblock.connect('font_family_changed', self._familyChanged)
        tblock.connect('font_style_changed', self._styleChanged)
        tblock.connect('font_color_changed', self._colorChanged)
        tblock.connect('font_weight_changed', self._weightChanged)
        tblock.connect('text_size_changed', self._sizeChanged)
        tblock.connect('text_angle_changed', self._angleChanged)
        tblock.connect('text_alignment_changed', self._alignmentChanged)
        tblock.connect('moved', self._moveText)

    def _textstyleChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 2:
            raise ValueError, "Invalid argument count: %d" % _alen
        _ts = args[0]
        if not isinstance(_ts, TextStyle):
            raise TypeError, "Invalid TextStyle type: " + `type(_ts)`
        _opts = args[1]
        if not isinstance(_opts, dict):
            raise TypeError, "Invalid option type: " + `type(_opts)`
        _data = {}
        _data['textstyle'] = _ts.getValues()
        for _k, _v in _opts:
            if _k == 'color':
                _data['color'] = _v.getColors()
            else:
                _data[_k] = _v
        self.saveUndoData('textstyle_changed', _data)

    def _textChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _text = args[0]
        if not isinstance(_text, types.StringTypes):
            raise TypeError, "Invalid text type: " + `type(_text)`
        self.saveUndoData('text_changed', _text)

    def _familyChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _family = args[0]
        if not isinstance(_family, types.StringTypes):
            raise TypeError, "Invalid family type: " + `type(_family)`
        self.saveUndoData('font_family_changed', _family)

    def _styleChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _style = args[0]
        if (_style != TextStyle.FONT_NORMAL and
            _style != TextStyle.FONT_OBLIQUE and
            _style != TextStyle.FONT_ITALIC):
            raise ValueError, "Invalid font style: " + str(_style)
        self.saveUndoData('font_style_changed', _style)

    def _colorChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _color = args[0]
        if not isinstance(_color, color.Color):
            raise TypeError, "Invalid color type: " + `type(_color)`
        self.saveUndoData('font_color_changed', _color.getColors())

    def _weightChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _weight = args[0]
        if (_weight != TextStyle.WEIGHT_NORMAL and
            _weight != TextStyle.WEIGHT_LIGHT and
            _weight != TextStyle.WEIGHT_BOLD and
            _weight != TextStyle.WEIGHT_HEAVY):
            raise ValueError, "Invalid text weight: %d" % _weight
        self.saveUndoData('font_weight_changed', _weight)

    def _sizeChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _size = args[0]
        if not isinstance(_size, float):
            raise TypeError, "Unexpected size type: " + `type(_size)`
        if _size < 0.0:
            raise ValueError, "Invalid text size: %g" % _size
        self.saveUndoData('text_size_changed', _size)

    def _angleChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _angle = args[0]
        if not isinstance(_angle, float):
            raise TypeError, "Unexpected angle type: " + `type(_angle)`
        if _angle > 360.0 or _angle < -360.0:
            _angle = math.fmod(_angle, 360.0)
        self.saveUndoData('text_angle_changed', _angle)

    def _alignmentChanged(self, tb, *args):
        _alen = len(args)
        if _alen < 1:
            raise ValueError, "Invalid argument count: %d" % _alen
        _align = args[0]
        if (_align != TextStyle.ALIGN_LEFT and
            _align != TextStyle.ALIGN_CENTER and
            _align != TextStyle.ALIGN_RIGHT):
            raise ValueError, "Invalid text alignment value: %d" % _align
        self.saveUndoData('text_alignment_changed', _align)

    def _moveText(self, tb, *args):
        _alen = len(args)
        if _alen < 2:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x = args[0]
        if not isinstance(_x, float):
            raise TypeError, "Unexpected type for x: " + `type(_x)`
        _y = args[1]
        if not isinstance(_y, float):
            raise TypeError, "Unexpected type for y: " + `type(_y)`
        self.saveUndoData('moved', _x, _y)

    def execute(self, undo, *args):
        util.test_boolean(undo)
        _alen = len(args)
        if len(args) == 0:
            raise ValueError, "No arguments to execute()"
        _tblock = self.getObject()
        _image = None
        _layer = _tblock.getParent()
        if _layer is not None:
            _image = _layer.getParent()
        _op = args[0]
        if (_op == 'text_changed' or
            _op == 'font_family_changed' or
            _op == 'font_style_changed' or
            _op == 'font_weight_changed' or
            _op == 'text_size_changed' or
            _op == 'text_angle_changed' or
            _op == 'text_alignment_changed'):
            if len(args) < 2:
                raise ValueError, "Invalid argument count: %d" % _alen
            _val = args[1]
            _get = TextBlockLog.__getops[_op]
            _sdata = _get(_tblock)
            self.ignore(_op)
            try:
                _set = TextBlockLog.__setops[_op]
                if undo:
                    _tblock.startUndo()
                    try:
                        _set(_tblock, _val)
                    finally:
                        _tblock.endUndo()
                else:
                    _tblock.startRedo()
                    try:
                        _set(_tblock, _val)
                    finally:
                        _tblock.endRedo()
            finally:
                self.receive(_op)
            self.saveData(undo, _op, _sdata)
        elif _op == 'moved':
            if _alen < 3:
                raise ValueError, "Invalid argument count: %d" % _alen
            _x = args[1]
            if not isinstance(_x, float):
                raise TypeError, "Unexpected type for x: " + `type(_x)`
            _y = args[2]
            if not isinstance(_y, float):
                raise TypeError, "Unexpected type for y: " + `type(_y)`
            _tx, _ty = _tblock.getLocation()
            self.ignore(_op)
            try:
                if undo:
                    _tblock.startUndo()
                    try:
                        _tblock.setLocation(_x, _y)
                    finally:
                        _tblock.endUndo()
                else:
                    _tblock.startRedo()
                    try:
                        _tblock.setLocation(_x, _y)
                    finally:
                        _tblock.endRedo()
            finally:
                self.receive(_op)
            self.saveData(undo, _op, _tx, _ty)
        elif _op == 'font_color_changed':
            if _image is None:
                raise RuntimeError, "TextBlock not stored in an Image"
            if _alen < 2:
                raise ValueError, "Invalid argument count: %d" % _alen
            _sdata = _tblock.getColor().getColors()
            self.ignore(_op)
            try:
                _color = None
                for _c in _image.getImageEntities('color'):
                    if _c.getColors() == args[1]:
                        _color = _c
                        break
                if undo:
                    _tblock.startUndo()
                    try:
                        _tblock.setColor(_color)
                    finally:
                        _tblock.endUndo()
                else:
                    _tblock.startRedo()
                    try:
                        _tblock.setColor(_color)
                    finally:
                        _tblock.endRedo()
            finally:
                self.receive(_op)
            self.saveData(undo, _op, _sdata)
        elif _op == 'textstyle_changed':
            if _image is None:
                raise RuntimeError, "TextBlock not stored in an Image"
            if _alen < 2:
                raise ValueError, "Invalid argument cound: %d" % _alen
            _data = args[1]
            _tsdata = _data['textstyle']            
            _sdata = {}
            _ts = _tblock.getTextStyle()
            _sdata['textstyle'] = _ts.getValues()
            _family = _tblock.getFamily()
            if _family != _ts.getFamily():
                _sdata['family'] = _family
            _style = _tblock.getStyle()
            if _style != _ts.getStyle():
                _sdata['style'] = _style
            _weight = _tblock.getWeight()
            if _weight != _ts.getWeight():
                _sdata['weight'] = _weight
            _color = _tblock.getColor()
            if _color != _ts.getColor():
                _sdata['color'] = _color.getColors()
            _size = _tblock.getSize()
            if abs(_size - _ts.getSize()) > 1e-10:
                _sdata['size'] = _size
            _angle = _tblock.getAngle()
            if abs(_angle - _ts.getAngle()) > 1e-10:
                _sdata['angle'] = _angle
            _align = _tblock.getAlignment()
            if _align != _ts.getAlignment():
                _sdata['align'] = _align
            self.ignore(_op)
            try:
                _tstyle = None
                for _ts in _image.getImageEntities('textstyle'):
                    if _ts.getName() != _tsdata['name']:
                        continue
                    if _ts.getFamily() != _tsdata['family']:
                        continue
                    if _ts.getStyle() != _tsdata['style']:
                        continue
                    if _ts.getWeight() != _tsdata['weight']:
                        continue
                    if _ts.getColor().getColors() != _tsdata['color']:
                        continue
                    if abs(_ts.getSize() - _tsdata['size']) > 1e-10:
                        continue
                    if abs(_ts.getAngle() - _tsdata['angle']) > 1e-10:
                        continue
                    if _ts.getAlignment() != _tsdata['align']:
                        continue
                    _tstyle = _ts
                    break
                if undo:
                    _tblock.startUndo()
                    try:
                        _tblock.setTextStyle(_tstyle)
                    finally:
                        _tblock.endUndo()
                else:
                    _tblock.startRedo()
                    try:
                        _tblock.setTextStyle(_tstyle)
                    finally:
                        _tblock.endRedo()
            finally:
                self.receive(_op)
            #
            # restore values differing from the TextStyle
            #
            _tblock.mute()
            try:
                _family = _data.get('family')
                if _family is not None:
                    _tblock.setFamily(_family)
                _style = _data.get('style')
                if _style is not None:
                    _tblock.setStyle(_style)
                _weight = _data.get('weight')
                if _weight is not None:
                    _tblock.setWeight(_weight)
                _color = _data.get('color')
                if _color is not None:
                    _col = None
                    for _c in _image.getImageEntities('color'):
                        if _c.getColors() == _color:
                            _col = _c
                            break
                    _tblock.setColor(_col)
                _size = _data.get('size')
                if _size is not None:
                    _tblock.setSize(_size)
                _angle = _data.get('angle')
                if _angle is not None:
                    _tblock.setAngle(_angle)
                _align = _data.get('align')
                if _align is not None:
                    _tblock.setAlignment(_align)
            finally:
                _tblock.unmute()
            self.saveData(undo, _op, _sdata)
        else:
            super(TextBlockLog, self).execute(undo, *args)
