#!/usr/bin/env python
#
#       text.py
#       
#       Copyright 2009 Sven Festersen <sven@sven-festersen.de>
#       
#       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, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.
"""
Contains the Label class.

Author: Sven Festersen (sven@sven-festersen.de)
"""
import cairo
import gobject
import gtk
import math
import pango
import pygtk

from pygtk_chart import basics
from pygtk_chart.chart_object import ChartObject


ANCHOR_BOTTOM_LEFT = 0
ANCHOR_TOP_LEFT = 1
ANCHOR_TOP_RIGHT = 2
ANCHOR_BOTTOM_RIGHT = 4
ANCHOR_CENTER = 5
ANCHOR_TOP_CENTER = 6
ANCHOR_BOTTOM_CENTER = 7
ANCHOR_LEFT_CENTER = 8
ANCHOR_RIGHT_CENTER = 9

UNDERLINE_NONE = pango.UNDERLINE_NONE
UNDERLINE_SINGLE = pango.UNDERLINE_SINGLE
UNDERLINE_DOUBLE = pango.UNDERLINE_DOUBLE
UNDERLINE_LOW = pango.UNDERLINE_LOW

STYLE_NORMAL = pango.STYLE_NORMAL
STYLE_OBLIQUE = pango.STYLE_OBLIQUE
STYLE_ITALIC = pango.STYLE_ITALIC

WEIGHT_ULTRALIGHT = pango.WEIGHT_ULTRALIGHT
WEIGHT_LIGHT = pango.WEIGHT_LIGHT
WEIGHT_NORMAL = pango.WEIGHT_NORMAL
WEIGHT_BOLD = pango.WEIGHT_BOLD
WEIGHT_ULTRABOLD = pango.WEIGHT_ULTRABOLD
WEIGHT_HEAVY = pango.WEIGHT_HEAVY


DRAWING_INITIALIZED = False
REGISTERED_LABELS = []


def begin_drawing():
    global DRAWING_INITIALIZED
    DRAWING_INITIALIZED = True
    
def finish_drawing():
    global REGISTERED_LABELS
    global DRAWING_INITIALIZED
    REGISTERED_LABELS = []
    DRAWING_INITIALIZED = False
    
def register_label(label):
    if DRAWING_INITIALIZED:
        REGISTERED_LABELS.append(label)
    
def get_registered_labels():
    if DRAWING_INITIALIZED:
        return REGISTERED_LABELS
    return []


class Label(ChartObject):
    """
    This class is used for drawing all the text on the chart widgets.
    It uses the pango layout engine.
    
    Properties
    ==========
    The Label class inherits properties from chart_object.ChartObject.
    Additional properties:
     - color (the label's color, type: gtk.gdk.Color)
     - text (text to display, type: string)
     - position (the label's position, type: pair of float)
     - anchor (the anchor that should be used to position the label,
       type: an anchor constant)
     - underline (sets the type of underline, type; an underline
       constant)
     - max-width (the maximum width of the label in px, type: int)
     - rotation (angle of rotation in degrees, type: int)
     - size (the size of the label's text in px, type: int)
     - slant (the font slant, type: a slant style constant)
     - weight (the font weight, type: a font weight constant)
     - fixed (sets whether the position of the label may be changed
       dynamicly or not, type: boolean)
     - wrap (sets whether the label's text should be wrapped if it's
       longer than max-width, type: boolean).
       
    Signals
    =======
    The Label class inherits signals from chart_object.ChartObject.
    """
    
    __gproperties__ = {"color": (gobject.TYPE_PYOBJECT,
                                "label color",
                                "The color of the label (a gtk.gdkColor)",
                                gobject.PARAM_READWRITE),
                        "text": (gobject.TYPE_STRING,
                                "label text",
                                "The text to show on the label.",
                                "", gobject.PARAM_READWRITE),
                        "position": (gobject.TYPE_PYOBJECT,
                                    "label position",
                                    "A pair of x,y coordinates.",
                                    gobject.PARAM_READWRITE),
                        "anchor": (gobject.TYPE_INT, "label anchor",
                                    "The anchor of the label.", 0, 9, 0,
                                    gobject.PARAM_READWRITE),
                        "underline": (gobject.TYPE_PYOBJECT,
                                    "underline text",
                                    "Set whether to underline the text.",
                                    gobject.PARAM_READWRITE),
                        "max-width": (gobject.TYPE_INT, "maximum width",
                                        "The maximum width of the label.",
                                        1, 99999, 99999,
                                        gobject.PARAM_READWRITE),
                        "rotation": (gobject.TYPE_INT, "rotation of the label",
                                    "The angle that the label should be rotated by in degrees.",
                                    0, 360, 0, gobject.PARAM_READWRITE),
                        "size": (gobject.TYPE_INT, "text size",
                                "The size of the text.", 0, 1000, 8,
                                gobject.PARAM_READWRITE),
                        "slant": (gobject.TYPE_PYOBJECT, "font slant",
                                "The font slant style.", 
                                gobject.PARAM_READWRITE),
                        "weight": (gobject.TYPE_PYOBJECT, "font weight",
                                "The font weight.", gobject.PARAM_READWRITE),
                        "fixed": (gobject.TYPE_BOOLEAN, "fixed",
                                    "Set whether the position of the label should be forced.",
                                    False, gobject.PARAM_READWRITE),
                        "wrap": (gobject.TYPE_BOOLEAN, "wrap text",
                                    "Set whether text should be wrapped.",
                                    False, gobject.PARAM_READWRITE)}
    
    def __init__(self, position, text, size=None,
                    slant=pango.STYLE_NORMAL,
                    weight=pango.WEIGHT_NORMAL,
                    underline=pango.UNDERLINE_NONE,
                    anchor=ANCHOR_BOTTOM_LEFT, max_width=99999,
                    fixed=False):
        ChartObject.__init__(self)
        self._position = position
        self._text = text
        self._size = size
        self._slant = slant
        self._weight = weight
        self._underline = underline
        self._anchor = anchor
        self._rotation = 0
        self._color = gtk.gdk.Color()
        self._max_width = max_width
        self._fixed = fixed
        self._wrap = True
        
        self._real_dimensions = (0, 0)
        self._real_position = (0, 0)
        self._line_count = 1
        
        self._context = None
        self._layout = None
        
    def do_get_property(self, property):
        if property.name == "visible":
            return self._show
        elif property.name == "antialias":
            return self._antialias
        elif property.name == "text":
            return self._text
        elif property.name == "color":
            return self._color
        elif property.name == "position":
            return self._position
        elif property.name == "anchor":
            return self._anchor
        elif property.name == "underline":
            return self._underline
        elif property.name == "max-width":
            return self._max_width
        elif property.name == "rotation":
            return self._rotation
        elif property.name == "size":
            return self._size
        elif property.name == "slant":
            return self._slant
        elif property.name == "weight":
            return self._weight
        elif property.name == "fixed":
            return self._fixed
        elif property.name == "wrap":
            return self._wrap
        else:
            raise AttributeError, "Property %s does not exist." % property.name

    def do_set_property(self, property, value):
        if property.name == "visible":
            self._show = value
        elif property.name == "antialias":
            self._antialias = value
        elif property.name == "text":
            self._text = value
        elif property.name == "color":
            self._color = value
        elif property.name == "position":
            self._position = value
        elif property.name == "anchor":
            self._anchor = value
        elif property.name == "underline":
            self._underline = value
        elif property.name == "max-width":
            self._max_width = value
        elif property.name == "rotation":
            self._rotation = value
        elif property.name == "size":
            self._size = value
        elif property.name == "slant":
            self._slant = value
        elif property.name == "weight":
            self._weight = value
        elif property.name == "fixed":
            self._fixed = value
        elif property.name == "wrap":
            self._wrap = value
        else:
            raise AttributeError, "Property %s does not exist." % property.name
        
    def _do_draw(self, context, rect):
        self._do_draw_label(context, rect)
        
    def _do_draw_label(self, context, rect):
        angle = 2 * math.pi * self._rotation / 360.0
        
        if self._context == None:
            label = gtk.Label()
            self._context = label.create_pango_context()          
        pango_context = self._context
        
        attrs = pango.AttrList()
        attrs.insert(pango.AttrWeight(self._weight, 0, len(self._text)))
        attrs.insert(pango.AttrStyle(self._slant, 0, len(self._text)))
        attrs.insert(pango.AttrUnderline(self._underline, 0,
                        len(self._text)))
        if self._size != None:
            attrs.insert(pango.AttrSize(1000 * self._size, 0,
                            len(self._text)))
        
        if self._layout == None:
            self._layout = pango.Layout(pango_context)
        layout = self._layout
        layout.set_text(self._text)
        layout.set_attributes(attrs)
        
        #find out where to draw the layout and calculate the maximum width
        width = rect.width
        if self._anchor in [ANCHOR_BOTTOM_LEFT, ANCHOR_TOP_LEFT,
                            ANCHOR_LEFT_CENTER]:
            width = rect.width - self._position[0]
        elif self._anchor in [ANCHOR_BOTTOM_RIGHT, ANCHOR_TOP_RIGHT,
                                ANCHOR_RIGHT_CENTER]:
            width = self._position[0]
        
        text_width, text_height = layout.get_pixel_size()
        width = width * math.cos(angle)
        width = min(width, self._max_width)
        
        if self._wrap:
            layout.set_wrap(pango.WRAP_WORD_CHAR)
        layout.set_width(int(1000 * width))
        
        x, y = get_text_pos(layout, self._position, self._anchor, angle)
        
        if not self._fixed:
            #Find already drawn labels that would intersect with the current one
            #and adjust position to avoid intersection.
            text_width, text_height = layout.get_pixel_size()
            real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle))
            real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle))
            
            other_labels = get_registered_labels()
            this_rect = gtk.gdk.Rectangle(int(x), int(y), int(real_width), int(real_height))
            for label in other_labels:
                label_rect = label.get_allocation()
                intersection = this_rect.intersect(label_rect)
                if intersection.width == 0 and intersection.height == 0:
                    continue
                
                y_diff = 0
                if label_rect.y <= y and label_rect.y + label_rect.height >= y:
                    y_diff = y - label_rect.y + label_rect.height
                elif label_rect.y > y and label_rect.y < y + real_height:
                    y_diff = label_rect.y - real_height - y
                y += y_diff
        
        #draw layout
        context.move_to(x, y)
        context.rotate(angle)
        context.set_source_rgb(*basics.color_gdk_to_cairo(self._color))
        context.show_layout(layout)
        context.rotate(-angle)
        context.stroke()
        
        #calculate the real dimensions
        text_width, text_height = layout.get_pixel_size()
        real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle))
        real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle))
        self._real_dimensions = real_width, real_height
        self._real_position = x, y
        self._line_count = layout.get_line_count()
        
        register_label(self)
        
    def get_calculated_dimensions(self, context, rect):
        angle = 2 * math.pi * self._rotation / 360.0
        
        if self._context == None:
            label = gtk.Label()
            self._context = label.create_pango_context()          
        pango_context = self._context
        
        attrs = pango.AttrList()
        attrs.insert(pango.AttrWeight(self._weight, 0, len(self._text)))
        attrs.insert(pango.AttrStyle(self._slant, 0, len(self._text)))
        attrs.insert(pango.AttrUnderline(self._underline, 0,
                        len(self._text)))
        if self._size != None:
            attrs.insert(pango.AttrSize(1000 * self._size, 0,
                            len(self._text)))
        
        if self._layout == None:
            self._layout = pango.Layout(pango_context)
        layout = self._layout
            
        layout.set_text(self._text)
        layout.set_attributes(attrs)
        
        #find out where to draw the layout and calculate the maximum width
        width = rect.width
        if self._anchor in [ANCHOR_BOTTOM_LEFT, ANCHOR_TOP_LEFT,
                            ANCHOR_LEFT_CENTER]:
            width = rect.width - self._position[0]
        elif self._anchor in [ANCHOR_BOTTOM_RIGHT, ANCHOR_TOP_RIGHT,
                                ANCHOR_RIGHT_CENTER]:
            width = self._position[0]
        
        text_width, text_height = layout.get_pixel_size()
        width = width * math.cos(angle)
        width = min(width, self._max_width)
        
        if self._wrap:
            layout.set_wrap(pango.WRAP_WORD_CHAR)
        layout.set_width(int(1000 * width))
        
        x, y = get_text_pos(layout, self._position, self._anchor, angle)
        
        if not self._fixed:
            #Find already drawn labels that would intersect with the current one
            #and adjust position to avoid intersection.
            text_width, text_height = layout.get_pixel_size()
            real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle))
            real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle))
            
            other_labels = get_registered_labels()
            this_rect = gtk.gdk.Rectangle(int(x), int(y), int(real_width), int(real_height))
            for label in other_labels:
                label_rect = label.get_allocation()
                intersection = this_rect.intersect(label_rect)
                if intersection.width == 0 and intersection.height == 0:
                    continue
                
                y_diff = 0
                if label_rect.y <= y and label_rect.y + label_rect.height >= y:
                    y_diff = y - label_rect.y + label_rect.height
                elif label_rect.y > y and label_rect.y < y + real_height:
                    y_diff = label_rect.y - real_height - y
                y += y_diff
        
        #calculate the dimensions
        text_width, text_height = layout.get_pixel_size()
        real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle))
        real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle))
        return real_width, real_height
        
    def set_text(self, text):
        """
        Use this method to set the text that should be displayed by
        the label.
        
        @param text: the text to display.
        @type text: string
        """
        self.set_property("text", text)
        self.emit("appearance_changed")
        
    def get_text(self):
        """
        Returns the text currently displayed.
        
        @return: string.
        """
        return self.get_property("text")
        
    def set_color(self, color):
        """
        Set the color of the label. color has to be a gtk.gdk.Color.
        
        @param color: the color of the label
        @type color: gtk.gdk.Color.
        """
        self.set_property("color", color)
        self.emit("appearance_changed")
        
    def get_color(self):
        """
        Returns the current color of the label.
        
        @return: gtk.gdk.Color.
        """
        return self.get_property("color")
        
    def set_position(self, pos):
        """
        Set the position of the label. pos has to be a x,y pair of
        absolute pixel coordinates on the widget.
        The position is not the actual position but the position of the
        Label's anchor point (see L{set_anchor} for details).
        
        @param pos: new position of the label
        @type pos: pair of (x, y).
        """
        self.set_property("position", pos)
        self.emit("appearance_changed")
        
    def get_position(self):
        """
        Returns the current position of the label.
        
        @return: pair of (x, y).
        """
        return self.get_property("position")
        
    def set_anchor(self, anchor):
        """
        Set the anchor point of the label. The anchor point is the a
        point on the label's edge that has the position you set with
        set_position().
        anchor has to be one of the following constants:
        
         - label.ANCHOR_BOTTOM_LEFT
         - label.ANCHOR_TOP_LEFT
         - label.ANCHOR_TOP_RIGHT
         - label.ANCHOR_BOTTOM_RIGHT
         - label.ANCHOR_CENTER
         - label.ANCHOR_TOP_CENTER
         - label.ANCHOR_BOTTOM_CENTER
         - label.ANCHOR_LEFT_CENTER
         - label.ANCHOR_RIGHT_CENTER
         
        The meaning of the constants is illustrated below:::
        
        
             ANCHOR_TOP_LEFT     ANCHOR_TOP_CENTER   ANCHOR_TOP_RIGHT
                            *           *           *
                              #####################
         ANCHOR_LEFT_CENTER * #         *         # * ANCHOR_RIGHT_CENTER
                              #####################
                            *           *           *
          ANCHOR_BOTTOM_LEFT   ANCHOR_BOTTOM_CENTER  ANCHOR_BOTTOM_RIGHT
          
        The point in the center is of course referred to by constant
        label.ANCHOR_CENTER.
        
        @param anchor: the anchor point of the label
        @type anchor: one of the constants described above.
        """
        
        self.set_property("anchor", anchor)
        self.emit("appearance_changed")
        
    def get_anchor(self):
        """
        Returns the current anchor point that's used to position the
        label. See L{set_anchor} for details.
        
        @return: one of the anchor constants described in L{set_anchor}.
        """
        return self.get_property("anchor")
        
    def set_underline(self, underline):
        """
        Set the underline style of the label. underline has to be one
        of the following constants:
        
         - label.UNDERLINE_NONE: do not underline the text
         - label.UNDERLINE_SINGLE: draw a single underline (the normal
           underline method)
         - label.UNDERLINE_DOUBLE: draw a double underline
         - label.UNDERLINE_LOW; draw a single low underline.
         
        @param underline: the underline style
        @type underline: one of the constants above.
        """    
        self.set_property("underline", underline)
        self.emit("appearance_changed")
        
    def get_underline(self):
        """
        Returns the current underline style. See L{set_underline} for
        details.
        
        @return: an underline constant (see L{set_underline}).
        """
        return self.get_property("underline")
        
    def set_max_width(self, width):
        """
        Set the maximum width of the label in pixels.
        
        @param width: the maximum width
        @type width: integer.
        """
        self.set_property("max-width", width)
        self.emit("appearance_changed")
        
    def get_max_width(self):
        """
        Returns the maximum width of the label.
        
        @return: integer.
        """
        return self.get_property("max-width")
        
    def set_rotation(self, angle):
        """
        Use this method to set the rotation of the label in degrees.
        
        @param angle: the rotation angle
        @type angle: integer in [0, 360].
        """
        self.set_property("rotation", angle)
        self.emit("appearance_changed")
        
    def get_rotation(self):
        """
        Returns the current rotation angle.
        
        @return: integer in [0, 360].
        """
        return self.get_property("rotation")
        
    def set_size(self, size):
        """
        Set the size of the text in pixels.
        
        @param size: size of the text
        @type size: integer.
        """
        self.set_property("size", size)
        self.emit("appearance_changed")
        
    def get_size(self):
        """
        Returns the current size of the text in pixels.
        
        @return: integer.
        """
        return self.get_property("size")
        
    def set_slant(self, slant):
        """
        Set the font slant. slat has to be one of the following:
        
         - label.STYLE_NORMAL
         - label.STYLE_OBLIQUE
         - label.STYLE_ITALIC
         
        @param slant: the font slant style
        @type slant: one of the constants above.
        """
        self.set_property("slant", slant)
        self.emit("appearance_changed")
        
    def get_slant(self):
        """
        Returns the current font slant style. See L{set_slant} for
        details.
        
        @return: a slant style constant.
        """
        return self.get_property("slant")
        
    def set_weight(self, weight):
        """
        Set the font weight. weight has to be one of the following:
        
         - label.WEIGHT_ULTRALIGHT
         - label.WEIGHT_LIGHT
         - label.WEIGHT_NORMAL
         - label.WEIGHT_BOLD
         - label.WEIGHT_ULTRABOLD
         - label.WEIGHT_HEAVY
         
        @param weight: the font weight
        @type weight: one of the constants above.
        """
        self.set_property("weight", weight)
        self.emit("appearance_changed")
        
    def get_weight(self):
        """
        Returns the current font weight. See L{set_weight} for details.
        
        @return: a font weight constant.
        """
        return self.get_property("weight")
        
    def set_fixed(self, fixed):
        """
        Set whether the position of the label should be forced
        (fixed=True) or if it should be positioned avoiding intersection
        with other labels.
        
        @type fixed: boolean.
        """
        self.set_property("fixed", fixed)
        self.emit("appearance_changed")
        
    def get_fixed(self):
        """
        Returns True if the label's position is forced.
        
        @return: boolean
        """
        return self.get_property("fixed")
        
    def set_wrap(self, wrap):
        """
        Set whether too long text should be wrapped.
        
        @type wrap: boolean.
        """
        self.set_property("wrap", wrap)
        self.emit("appearance_changed")
        
    def get_wrap(self):
        """
        Returns True if too long text should be wrapped.
        
        @return: boolean.
        """
        return self.get_property("wrap")
        
    def get_real_dimensions(self):
        """
        This method returns a pair (width, height) with the dimensions
        the label was drawn with. Call this method I{after} drawing
        the label.
        
        @return: a (width, height) pair.
        """
        return self._real_dimensions
        
    def get_real_position(self):
        """
        Returns the position of the label where it was really drawn.
        
        @return: a (x, y) pair.
        """
        return self._real_position
        
    def get_allocation(self):
        """
        Returns an allocation rectangle.
        
        @return: gtk.gdk.Rectangle.
        """
        x, y = self._real_position
        w, h = self._real_dimensions
        return gtk.gdk.Rectangle(int(x), int(y), int(w), int(h))
        
    def get_line_count(self):
        """
        Returns the number of lines.
        
        @return: int.
        """
        return self._line_count
    
        
def get_text_pos(layout, pos, anchor, angle):
    """
    This function calculates the position of bottom left point of the
    layout respecting the given anchor point.
    
    @return: (x, y) pair
    """
    text_width_n, text_height_n = layout.get_pixel_size()
    text_width = text_width_n * abs(math.cos(angle)) + text_height_n * abs(math.sin(angle))
    text_height = text_height_n * abs(math.cos(angle)) + text_width_n * abs(math.sin(angle))
    height_delta = text_height - text_height_n
    x, y = pos
    ref = (0, -text_height)
    if anchor == ANCHOR_TOP_LEFT:
        ref = (0, 0)
    elif anchor == ANCHOR_TOP_RIGHT:
        ref = (-text_width, height_delta)
    elif anchor == ANCHOR_BOTTOM_RIGHT:
        ref = (-text_width, -text_height)
    elif anchor == ANCHOR_CENTER:
        ref = (-text_width / 2, -text_height / 2)
    elif anchor == ANCHOR_TOP_CENTER:
        ref = (-text_width / 2, 0)
    elif anchor == ANCHOR_BOTTOM_CENTER:
        ref = (-text_width / 2, -text_height)
    elif anchor == ANCHOR_LEFT_CENTER:
        ref = (0, -text_height / 2)
    elif anchor == ANCHOR_RIGHT_CENTER:
        ref = (-text_width, -text_height / 2)
    x += ref[0]
    y += ref[1]
    return x, y