#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2008 Adriano Monteiro Marques
#
# Author: Francesco Piccinno <stack.box@gmail.com>
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""
This module provides:
- HIGTooltipData
- HIGTooltip
"""

import gtk
import gobject
import cairo

from umit.core.I18N import _

class HIGTooltipData(object):
    """
    A class to holds tooltip messages information
    """

    def __init__(self, message, \
                 title=_("<span size=\"medium\"><b>Information</b></span>"), \
                 stock=gtk.STOCK_DIALOG_INFO, file=None):
        """
        Create a HIGTooltipData object

        @param message the message to show (with markups)
        @param the title of ballons (with markups)
        @param stock the stock image to use
        @param file the file to use with image
        """
        self.title = title
        self.message = message
        self.stock = stock
        self.file = file
        self.widgets = []

    def append_widget(self, widget, expand=True, fill=True, padding=0, show=True):
        self.widgets.append(((widget, expand, fill, padding), show))

class HIGTooltip(gtk.Window):
    """
    This is a singleton class so not create
    other instances
    """

    __gtype_name__ = "HIGTooltip"
    __instance = None

    def __init__(self):
        """
        Create a HIGTooltip object
        """
        gtk.Window.__init__(self)#, gtk.WINDOW_POPUP)
        self.set_decorated(False)

        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_MENU) # should be tooltip
        self.set_app_paintable(True)
        self.set_redraw_on_allocate(False)

        self.__create_widgets()
        self.__pack_widgets()

        self._widgets = {}
        self._hover = None
        self._interval = 1000
        self._timeout_id = None
        self._timeout_started = False

        # Used to track placement
        self._is_top = False
        self._is_left = False

        self._tail_height = 20
        self._tail_width = 10
        self._tail_offset = 10

    def __create_widgets(self):
        self._title = gtk.Label()
        self._message = gtk.Label()
        self._icon = gtk.image_new_from_stock(gtk.STOCK_DIALOG_INFO, \
                                              gtk.ICON_SIZE_MENU)

        self._message.set_alignment(0, 0.5)

        # We use label as separator
        
        self._top = gtk.Label()
        self._bottom = gtk.Label()
        self._right = gtk.Label()
        self._left = gtk.Label()

        self.vbox = gtk.VBox()

    def __pack_widgets(self):
        hbox = gtk.HBox(False, 2)
        hbox.pack_start(self._icon, False, False, 0)
        hbox.pack_start(self._title)
        hbox.set_spacing(4)

        self.vbox.pack_start(hbox, False, False, 0)
        self.vbox.pack_start(gtk.HSeparator(), False, False, 0)
        self.vbox.pack_start(self._message, False, False, 0)

        self.vbox.set_spacing(4)
        self.vbox.set_border_width(10)

        table = gtk.Table(3, 3)
        table.attach(self._top, 1, 2, 0, 1, yoptions=gtk.SHRINK)
        table.attach(self._bottom, 1, 2, 2, 3, yoptions=gtk.SHRINK)
        table.attach(self._left, 0, 1, 0, 3, xoptions=gtk.SHRINK)
        table.attach(self._right, 2, 3, 0, 3, xoptions=gtk.SHRINK)
        
        table.attach(self.vbox, 1, 2, 1, 2)

        table.show_all()
        self.add(table)

    def __update_gradient(self):
        alloc = self.allocation

        x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
        x, y = 0, 0

        # We use yellow colors
        #start = self.to_cairo_color(self.style.light[gtk.STATE_NORMAL])
        #end   = self.to_cairo_color(self.style.mid[gtk.STATE_NORMAL])

        start = gtk.gdk.color_parse("#FFFFDD")
        end   = gtk.gdk.color_parse("#D0D0B0")

        start = self.to_cairo_color(start)
        end   = self.to_cairo_color(end)

        self.gradient = cairo.LinearGradient(x, y, x, y + h)
        self.gradient.add_color_stop_rgb(0.3, *start)
        self.gradient.add_color_stop_rgb(0.9, *end)

    def do_size_allocate(self, alloc):
        self.__update_gradient()

        return gtk.Window.do_size_allocate(self, alloc)

    def __draw_round_rect(self, cr, x, y, w, h, radius_x=5, radius_y=5):
        ARC_TO_BEZIER = 0.55228475

        if radius_x > w - radius_x:
            radius_x = w / 2
        if radius_y > h - radius_y:
            radius_y = h / 2
    
        c1 = ARC_TO_BEZIER * radius_x
        c2 = ARC_TO_BEZIER * radius_y
    
        cr.new_path()
        cr.move_to(x + radius_x, y)
        cr.rel_line_to(w - 2 * radius_x, 0.0)
        cr.rel_curve_to(c1, 0.0, radius_x, c2, radius_x, radius_y)
        cr.rel_line_to(0, h - 2 * radius_y)
        cr.rel_curve_to(0.0, c2, c1 - radius_x, radius_y, -radius_x, radius_y)
        cr.rel_line_to(-w + 2 * radius_x, 0)
        cr.rel_curve_to(-c1, 0, -radius_x, -c2, -radius_x, -radius_y)
        cr.rel_line_to(0, -h + 2 * radius_y)
        cr.rel_curve_to(0.0, -c2, radius_x - c1, -radius_y, radius_x, -radius_y)
        cr.close_path()

    def set_source_color(self, cr, color):
        cr.set_source_rgb(
                *self.to_cairo_color(color)
        )

    def to_cairo_color(self, color):
        t = (
            float(float(color.red >> 8) / 255.0),
            float(float(color.green >> 8) / 255.0),
            float(float(color.blue >> 8) / 255.0)
        )
        return t

    def __draw_borders(self, alloc, cr=None):
        x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
        x, y = 0, 0

        shaping = False

        if not cr:
            shaping = True

            bmp = gtk.gdk.Pixmap(None, w, h, 1)
            cr = bmp.cairo_create()

            # Clear the bmp
            cr.save()
            cr.rectangle(0, 0, w, h)
            cr.set_operator(cairo.OPERATOR_CLEAR)
            cr.fill()
            cr.restore()

            cr.set_source_rgb(1, 1, 1)
        else:
            # Set the gradient
            cr.set_source(self.gradient)

        if self._is_top:
            # If top we should decrease height
            h -= self._tail_height
        else:
            # increase y
            y += self._tail_height
            h -= self._tail_offset * 2 # investigate on it

        if self._is_left:
            # If left we should decrease width
            w -= self._tail_width - self._tail_offset
        else:
            # increase x
            x += self._tail_width - self._tail_offset

        self.__draw_round_rect(cr, x, y, w, h, 8, 10)
        cr.fill_preserve()

        if not shaping:
            # Set the color for border
            cr.set_source_rgb( \
                *self.to_cairo_color(self.style.dark[gtk.STATE_NORMAL]) \
            )
            cr.set_line_width(2)
            cr.stroke()

            cr.set_source(self.gradient)

        # Restore my precious coordinates :D
        x, y, w, h = alloc.x, alloc.y, alloc.width, alloc.height
        x, y = 0, 0

        cr.new_path()
        cr.line_to(*self.__draw_tail(cr, (x, y, w, h)))
        cr.close_path()

        cr.fill()

        if shaping:
            self.shape_combine_mask(bmp, 0, 0)
        else:
            # Set the color for border
            cr.set_line_width(1.5)
            cr.set_source_rgb( \
                *self.to_cairo_color(self.style.dark[gtk.STATE_NORMAL]) \
            )

            # I need the coords
            x, y, w, h = self.__draw_tail(None, (x, y, w, h))

            cr.move_to(x, y)
            cr.rel_line_to(w, h)
            cr.move_to(x, y)

            # Dirty hack here :D
            w *= 3
            w = float(w)

            if w < 0:
                w += 2
            else:
                w -= 2

            cr.rel_line_to(w, h)
            
            cr.stroke()

    def __draw_tail(self, cr, alloc):
        # Dirty zipped function

        x, y, w, h = alloc

        _w = self._tail_width
        _h = self._tail_height

        if cr:
            _h += 2

        start_x = w
        start_y = y

        if self._is_left:
            _w *= -1
        else:
            start_x = x
        
        if self._is_top:
            _h *= -1
            start_y = h

        if cr:
            cr.move_to(start_x, start_y)
            cr.rel_line_to(_w, _h)
            cr.rel_line_to(_w * 2, 0)

            return (start_x, start_y)
        else:
            return (start_x, start_y, _w, _h)

    def do_realize(self):
        gtk.Window.do_realize(self)
        self.__update_gradient()

    def do_expose_event(self, evt):
        # Reshape
        self.__draw_borders(self.allocation, None)

        # Redraw man!
        self.__draw_borders(self.allocation, self.window.cairo_create())

        return gtk.Window.do_expose_event(self, evt)

    def __on_widget_event(self, widget, evt):

        if evt.type == gtk.gdk.LEAVE_NOTIFY:
            self._hover = None
            self._timeout_started = False

            if self._timeout_id:
                gobject.source_remove(self._timeout_id)
                self._timeout_id = None

            self.hide()
        elif evt.type == gtk.gdk.ENTER_NOTIFY:

            if not self._timeout_id:
                self._hover = widget
                self._timeout_started = True

                self._timeout_id = gobject.timeout_add(self._interval, \
                                                       self.__show_tooltip, \
                                                       widget)

    @staticmethod
    def add_widget(widget, data):
        """
        Add a tooltip to the widget

        @param widget the widget
        @param data the HIGTooltipData
        """
        assert isinstance(data, HIGTooltipData), \
               "must be a HIGTooltipData object"

        if widget in HIGTooltip.get_instance().widgets:
            return

        widget.add_events(
            gtk.gdk.ALL_EVENTS_MASK |        \
            gtk.gdk.LEAVE_NOTIFY_MASK |      \
            gtk.gdk.PROXIMITY_IN_MASK |      \
            gtk.gdk.ENTER_NOTIFY_MASK |      \
            gtk.gdk.FOCUS_CHANGE_MASK |      \
            gtk.gdk.POINTER_MOTION_MASK |    \
            gtk.gdk.POINTER_MOTION_HINT_MASK
        )
        widget.connect('event', HIGTooltip.get_instance().__on_widget_event)

        HIGTooltip.get_instance().widgets[widget] = data
    
    def show_at(self, w, data, x, y, time=None):
        """
        Show the tooltip at location

        (make x, y optional)
        """

        assert self != HIGTooltip.__instance, \
               "You should create another HIGTooltip object to do it"

        if self._timeout_started or \
           self.flags() & gtk.VISIBLE or \
           self._hover:
            return False

        # We pack the childs now
        for child, show in data.widgets:
            self.vbox.pack_start(*child)

            if show:
                child[0].show()

        if isinstance(w, gtk.Widget):
            toplevel = w.get_toplevel()

            if toplevel:
                toplevel.connect('configure-event', self.__close_on_event)

        self._hover = w
        self._timeout_started = True
        self.__show_tooltip(w, data, x, y)

        if time:
            self._timeout_id = gobject.timeout_add(time, self.close_and_destroy)
    def __close_on_event(self, w, event):
        self.close_and_destroy()

    def close_and_destroy(self):
        """
        Hide the widget resetting values used and also destroy window
        """
        assert self != HIGTooltip.__instance, \
               "You should create another HIGTooltip object to do it"

        self._hover = None
        self._timeout_started = False

        if self._timeout_id:
            gobject.source_remove(self._timeout_id)
            self._timeout_id = None

        self.hide()
        #self.destroy()

        return False

    def __show_tooltip(self, w, data=None, x=None, y=None):
        if not self._timeout_started or \
           self.flags() & gtk.VISIBLE or \
           self._hover != w:
            return False

        if not data:
            window = w.get_toplevel().window

            x, y = window.get_position()
            px, py, pmask = window.get_pointer()

            x += px; y += py

        # We need to choose where show the ballons
        # bottom, top, right, left, bright, bleft, tright, tleft

        offset = 100
        s_w, s_h = gtk.gdk.screen_width(), gtk.gdk.screen_height()

        if x > s_w - offset: self._is_left = True
        else: self._is_left = False

        if y > s_h - offset: self._is_top = True
        else: self._is_top = False

        if not data:
            data = self._widgets[w]

        self.title = data.title
        self.message = data.message

        if data.stock:
            self.icon = data.stock
        else:
            self.set_icon_from_file(data.file)

        self.bottom = -1
        self.right = -1
        self.left = -1
        self.top = -1

        if self._is_top:
            self.top = 0
            y -= 100
        else:
            self.bottom = 0
            y += 5

        if self._is_left:
            self.left = 0
            x -= 150
        else:
            self.right = 0
            x += 5

        self.move(x, y)
        self.resize(150, 100)
        self.show()

        return False

    @staticmethod
    def get_instance():
        "Return the instance of HIGTooltip"
        if not HIGTooltip.__instance:
            HIGTooltip.__instance = HIGTooltip()

        return HIGTooltip.__instance

    def get_icon(self):
        return self._icon

    def set_icon(self, value):
        self._icon.set_from_stock(value, gtk.ICON_SIZE_MENU)

    def set_icon_from_file(self, value):
        self._icon.set_from_file(value)

    def get_title(self):
        return self._title.get_text()

    def set_title(self, txt):
        self._title.set_text(txt)
        self._title.set_use_markup(True)

    def get_message(self):
        return self._message.get_text()

    def set_message(self, txt):
        self._message.set_text(txt)
        self._message.set_use_markup(True)

    def get_widgets(self):
        return self._widgets

    widgets = property(get_widgets)
    icon = property(get_icon, set_icon)
    title = property(get_title, set_title)
    message = property(get_message, set_message)

    def get_top(self):
        return 0
    def set_top(self, x):
        self._top.set_size_request(-1, x)

    def get_bottom(self):
        return 0
    def set_bottom(self, x):
        self._bottom.set_size_request(-1, x)


    top = property(get_top, set_top)
    bottom = property(get_bottom, set_bottom)

    def get_left(self):
        return 0
    def set_left(self, y):
        self._left.set_size_request(y, -1)

    def get_right(self):
        return 0
    def set_right(self, y):
        self._right.set_size_request(y, -1)

    left = property(get_left, set_left)
    right = property(get_right, set_right)

if __name__ == "__main__":
    w = gtk.Window()
    b = gtk.Button(stock=gtk.STOCK_OK)
    
    HIGTooltip.add_widget(
        b,
        HIGTooltipData(
            "<b>This</b> is an error:\n<tt>what the hell</tt>",
            stock=gtk.STOCK_DIALOG_ERROR
        )
    )
    bb = gtk.VBox()
    bb.pack_start(b)

    b = gtk.Button(stock=gtk.STOCK_CANCEL)
    HIGTooltip.add_widget(
        b,
        HIGTooltipData(
            "This is a message with a throbber as image :)",
            stock=None,
            file="share/pixmaps/Throbber.gif"
        )
    )
    bb.pack_start(b)
    
    w.add(bb)
    w.show_all()
    
    w.connect('delete-event', lambda *w: gtk.main_quit())
    gtk.main()
