"""PIDDLE implementation that uses a Gtk+ GtkDrawingArea object as the
output device.

This module offers four classes which support the piddle.Canvas
interface, two of which provide the standard constructor signature.
One serves as the standard interface for creating PIDDLE canvas
objects, offering the full range of canvas attributes (interactivity,
updatability, and an info line).

Classes
-------

Canvas
    Basic canvas class that offers all the display interfaces but
    doesn't provide support for interactivity (or the load required to
    fully support that aspect of the API).  The canvas uses double
    buffering to eliminate flicker, and supports arbitrary resizing.
    A gtk.GtkDrawingArea widget may be passed to the constructor.

InteractiveCanvas
    Subclass of Canvas which adds interactivity support for all three
    on*() methods.  This class makes a fair effort to reduce the
    required overhead of implementing the dynamic interface, but can't
    avoid too much of it.  Only use an interactive Canvas if you
    actually need one.  Requires that both the drawing area and the
    toplevel window (gtk.GtkWindow or gtk.GtkDialog) be passed to the
    constructor; the toplevel window is required in order to set up
    the event handlers properly.

DialogCanvas
    Sublass of InteractiveCanvas which supports the standard PIDDLE
    constructor signature in all but name.

GTKCanvas
    This is the 'standard' PIDDLE interface; it offers the standard
    constructor as well as the required name.  It provides a simple
    top-level window (gtk.GtkWindow) that contains nothing but the
    canvas's drawing area and a simple status bar at the bottom.  The
    status bar is used to support the piddle.Canvas setInfoLine()
    interface.  Once created, the creator must enter the Gtk+ main
    loop (or already be running the main loop).

Limitations
-----------

The Canvas class doesn't yet support rotated text.  This will be added
as time allows.  The basic functionality for all required fonts
appears to be working for non-rotated text for the required fonts.
Eventually, I'd like to support alternative font management schemes
through the piddle.Canvas interface, allowing the use of the Gtk+
native X font support for applications which don't need rotated text
(allowing a simple and fast implementation), and offering t1lib and
FreeType rendering for applications which need stronger text support.

The .drawImage() API hasn't been implemented yet; this will come as I
understand more about the image models offered with Gtk+ and have a
chance to play with the recent versions of the Python Imaging Library.

"""
__author__ = "Fred L. Drake, Jr.  <fdrake@acm.org>"
__version__ = '$Revision: 1.1 $'

import gtk
import string

# Why the import *?  Because that's how the examples do it, so try to
# emulate the exported symbols as much as possible.
from piddle import *
import piddle
_piddle = piddle
del piddle

# This is useful to get to GDK functions and constants:
_gtk = gtk._gtk
_GDK = gtk.GDK


_DEFAULT_FONT_FACE = "helvetica"

# compute the number of pixels for each typographer's point:
_PIXELS_PER_POINT = ((25.4 * _gtk.gdk_screen_height())
                     / _gtk.gdk_screen_height_mm()) / 72.0


class BasicCanvas(_piddle.Canvas):
    def __init__(self, area=None):
        if area is None:
            area = gtk.GtkDrawingArea()
        self.__area = area
        self.__setup = 0
        self.__color_cache = {}
        self.backgroundColor = white
        self.__buffer = None
        self.__buffer_size = (-1, -1)
        self.__area.connect("expose_event", self.__expose_event)
        self.__area.connect("configure_event", self.__configure_event)

        _piddle.Canvas.__init__(self)

    def isInteractive(self):
        return 0

    def canUpdate(self):
        return 1

    def clear(self, background=None):
        if background is not None:
            self.backgroundColor = background
        if self.__buffer:
            width, height = self.__buffer_size
            self.__buffer_size = (-1, -1)
            self.__buffer = None
            buffer = self.__ensure_size(width, height)
            self.__area.draw_pixmap(self.__gc, buffer,
                                   0, 0, 0, 0, width, height)

    def flush(self):
        if self.__buffer:
            width, height = self.__buffer_size
            self.__area.draw_pixmap(self.__gc, self.__buffer,
                                    0, 0, 0, 0, width, height)

    def drawLine(self, x1, y1, x2, y2, color=None, width=None):
        # We could just call drawLines(), or drawLines() could call here,
        # but that would just get slow.
        if color == transparent:
            return
        if color is None:
            color = self.defaultLineColor
            if color == transparent:
                return
        if width is None:
            width = self.defaultLineWidth
        buffer = self.__ensure_size(x2+width, y2+width)
        gc = self.__gc
        gc.foreground = self.__get_color(color)
        gc.line_width = width
        _gtk.gdk_draw_line(buffer, gc, x1, y1, x2, y2)

    def drawLines(self, lineList, color=None, width=None):
        if color == transparent:
            return
        if color is None:
            color = self.defaultLineColor
            if color == transparent:
                return
        if width is None:
            width = self.defaultLineWidth
        # force everything to the nearest integer,
        # and make sure the canvas is big enough:
        iwidth = iheight = 0
        for i in range(len(lineList)):
            x1, y1, x2, y2 = map(int, map(round, lineList[i]))
            iwidth = max(iwidth, x1, x2)
            iheight = max(iheight, y1, y2)
        #
        buffer = self.__ensure_size(iwidth+width, iheight+width)
        gc = self.__gc
        gc.foreground = self.__get_color(color)
        gc.line_width = width
        _gtk.gdk_draw_segments(buffer, gc, lineList)

    def drawString(self, s, x, y, font=None, color=None, angle=0.0):
        if color == transparent:
            return
        if color is None:
            color = self.defaultLineColor
            if color == transparent:
                return
        if "\n" in s or "\r" in s:
            self.drawMultiLineString(s, x, y, font, color, angle)
            return
        angle = int(round(angle))
        if angle != 0:
            raise NotImplementedError, "rotated text not implemented"
        if font is None:
            font = self.defaultFont
        lines = string.split(s, "\n")
        gdk_font = _font_to_gdkfont(font)
        textwidth = gdk_font.measure(s)
        width = max(x, x + textwidth)
        height = max(y, y + gdk_font.descent)
        buffer = self.__ensure_size(width, height)
        gc = self.__gc
        gc.foreground = self.__get_color(color)
        #gc.font = gdk_font
        _gtk.gdk_draw_text(buffer, gdk_font, gc, x, y, s)
        if font.underline:
            gc.line_width = 1
            y = y + gdk_font.descent
            _gtk.gdk_draw_line(buffer, gc, x, y, x + textwidth, y)

    def fontHeight(self, font=None):
        if font is None:
            font = self.defaultFont
        return 1.2 * font.size

    def fontAscent(self, font=None):
        if font is None:
            font = self.defaultFont
        gdk_font = _font_to_gdkfont(font)
        return gdk_font.ascent

    def fontDescent(self, font=None):
        if font is None:
            font = self.defaultFont
        gdk_font = _font_to_gdkfont(font)
        return gdk_font.descent

    def stringWidth(self, s, font=None):
        if font is None:
            font = self.defaultFont
        return _font_to_gdkfont(font).measure(s)

    def drawPolygon(self, pointlist, edgeColor=None, edgeWidth=None,
                    fillColor=None, closed=0):
        if len(pointlist) < 3:
            raise ValueError, "too few points in the point list"
        # XXX lots more should be checked
        if edgeColor is None:
            edgeColor = self.defaultLineColor
        if edgeWidth is None:
            edgeWidth = self.defaultLineWidth
        if fillColor is None:
            fillColor = self.defaultFillColor
        # force everything to the nearest integer,
        # and make sure the canvas is big enough:
        iwidth = iheight = 0
        for i in range(len(pointlist)):
            x, y = pointlist[i]
            point = (int(round(x)), int(round(y)))
            x, y = point
            iwidth = max(iwidth, x)
            iheight = max(iheight, y)
            pointlist[i] = point
        buffer = self.__ensure_size(iwidth+edgeWidth, iheight+edgeWidth)
        #
        gc = self.__gc
        if fillColor != transparent:
            filled = 1
            gc.foreground = self.__get_color(fillColor)
            gc.line_width = 1
            _gtk.gdk_draw_polygon(buffer, gc, 1, pointlist)
        if edgeColor != transparent:
            gc.foreground = self.__get_color(edgeColor)
            gc.line_width = edgeWidth
            if closed:
                _gtk.gdk_draw_polygon(buffer, gc, 0, pointlist)
            else:
                _gtk.gdk_draw_lines(buffer, gc, pointlist)

    def drawRect(self, x1, y1, x2, y2, edgeColor=None, edgeWidth=None,
                 fillColor=None):
        if edgeColor is None:
            edgeColor = self.defaultLineColor
        if edgeWidth is None:
            edgeWidth = self.defaultLineWidth
        if fillColor is None:
            fillColor = self.defaultFillColor
        w = max(x1 + edgeWidth, x2 + edgeWidth)
        h = max(y1 + edgeWidth, y2 + edgeWidth)
        buffer = self.__ensure_size(w, h)
        gc = self.__gc
        if fillColor != transparent:
            gc.foreground = self.__get_color(fillColor)
            gc.line_width = 1
            _gtk.gdk_draw_rectangle(buffer, gc, 1, x1, y1, x2-x1, y2-y1)
        if edgeColor != transparent:
            gc.foreground = self.__get_color(edgeColor)
            gc.line_width = edgeWidth
            _gtk.gdk_draw_rectangle(buffer, gc, 0, x1, y1, x2-x1, y2-y1)


    # Class-specific interfaces:

    def get_drawing_area(self):
        return self.__area

    def ensure_size(self, width, height):
        # like __ensure_size(), but doesn't return buffer
        if (width <= 0) or (height <= 0):
            raise ValueError, "width and height must both be positive"
        self.__ensure_size(width, height)


    # Interfaces for subclasses:

    def __ensure_size(self, width, height):
        if not (width and height):
            return
        old_width, old_height = self.__buffer_size
        width = max(width, old_width)
        height = max(height, old_height)
        if not self.__setup:
            w = self.__area.get_window()
            self.__gc = w.new_gc()
            self.__cmap = w.colormap
            self.__setup = 1
            # surely there's a better way to get this?
            self.__depth = 24
        if (width, height) != self.__buffer_size or not self.__buffer:
            new_pixmap = _gtk.gdk_pixmap_new(
                None, width, height, self.__depth)
            gc = self.__gc
            if self.backgroundColor == transparent:
                self.backgroundColor = white
            c = self.__get_color(self.backgroundColor)
            gc.foreground = c
            y = (height + 1) / 2
            gc.line_width = int(y * 2)
            _gtk.gdk_draw_line(new_pixmap, gc, 0, y, width, y)
            if self.__buffer:
                # copy from old buffer to new buffer
                _gtk.gdk_draw_pixmap(new_pixmap, gc, self.__buffer,
                                     0, 0, 0, 0, old_width, old_height)
            self.__buffer = new_pixmap
            self.__buffer_size = (width, height)
            return new_pixmap
        else:
            return self.__buffer


    # Internal interfaces:

    def __get_color(self, color):
        try:
            return self.__color_cache[color]
        except KeyError:
            _int = int
            c = self.__cmap.alloc(_int(color.red * 0xffff),
                                  _int(color.green * 0xffff),
                                  _int(color.blue * 0xffff))
            self.__color_cache[color] = c
            return c

    __font_cache = {}
    def __get_font(self, font=None):
        if font is None:
            font = self.defaultFont
        # determine the font face:
        key = _font_to_key(font)

    def __configure_event(self, area, event):
        buffer = self.__ensure_size(event.x + event.width,
                                   event.y + event.height)
        width, height = self.__buffer_size
        if (width > 0) and (height > 0):
            self.__area.draw_pixmap(self.__gc, buffer, 0, 0, 0, 0,
                                    width, height)
        self.size = self.__area.get_allocation()[2:]

    def __expose_event(self, area, event):
        x, y, width, height = event.area
        buffer = self.__ensure_size(width, height)
        self.__area.draw_pixmap(self.__gc, buffer, x, y, x, y,
                                width, height)


def _font_to_gdkfont(font):
    key = _font_to_key(font)
    xlfd = _fontkey_to_xlfd(key)
    return _xlfd_to_gdkfont(xlfd)


def _font_to_key(font):
    face = _DEFAULT_FONT_FACE
    if type(font.face) is type(''):
        try:
            face = _get_font_face(font.face)
        except KeyError:
            pass
    elif font.face is None:
        # we should use the default face
        pass
    else:
        for f in font.face:
            try:
                face = _get_font_face(f)
            except KeyError:
                pass
            else:
                break
    # weight & shaping attributes
    weight = _face_weight_map[face][font.bold and 1 or 0]
    slant = _face_slant_map[face][font.italic and 1 or 0]
    width = _face_width_map.get(face, "*")
    # If a scalable font isn't available, we might need to choose the
    # best fit from what's available:
    pixels = int(round(font.size * _PIXELS_PER_POINT))
    # 'charset' includes both registry and encoding
    if face == "symbol":
        charset = "adobe-fontspecific"
    else:
        charset = "iso8859-1"
    return (face, weight, slant, width, pixels, charset)

_face_weight_map = {
    # face: (normal, bold)
    "courier": ("medium", "bold"),
    "helvetica": ("medium", "bold"),
    "symbol": ("medium", "medium"),
    "times": ("medium", "bold"),
    }
_face_slant_map = {
    # face: (normal, italic)
    "courier": ("r", "i"),
    "helvetica": ("r", "o"),
    "symbol": ("r", "r"),
    "times": ("r", "i"),
    }
_face_width_map = {
    # face: normal-width-specifier
    "courier": "normal",
    "helvetica": "normal",
    "symbol": "normal",
    "times": "normal",
    }


def _get_font_face(face):
    try:
        return _font_face_map[face]
    except KeyError:
        lface = string.lower(face)
        # Raises KeyError if we just don't have it.
        face = _font_face_map[lface]
        _font_face_map[face] = face
        return face

_font_face_map = {
    "courier": "courier",
    "monospaced": "courier",
    "helvetica": "helvetica",
    "sansserif": "helvetica",
    "serif": "times",
    "symbol": "symbol",
    "times": "times",
    }


def _fontkey_to_xlfd(key):
    return "-*-%s-%s-%s-%s-*-%s-*-*-*-*-*-%s" % key


def _xlfd_to_gdkfont(xlfd, cache={}):
    try:
        return cache[xlfd]
    except KeyError:
        gdkfont = _gtk.gdk_font_load(xlfd)
        cache[xlfd] = gdkfont
        return gdkfont


class InteractiveCanvas(BasicCanvas):
    def __init__(self, area, window):
        # XXX set up the event handlers
        window.set_events(_GDK.BUTTON1_MOTION_MASK
                          | _GDK.BUTTON_RELEASE_MASK
                          | _GDK.KEY_PRESS_MASK
                          | _GDK.POINTER_MOTION_MASK
                          | _GDK.POINTER_MOTION_HINT_MASK)
        window.connect("event", self.__event)
        self.__get_allocation = area.get_allocation
        BasicCanvas.__init__(self, area)

    def isInteractive(self):
        return 1

    def __event(self, widget, event):
        if event.type == _GDK.ENTER_NOTIFY:
            x, y, ok = self.__check_coords(event.x, event.y)
            if ok:
                self.onOver(self, x, y)
        elif event.type == _GDK.MOTION_NOTIFY:
            x, y = widget.get_pointer()
            x, y, ok = self.__check_coords(x, y)
            if ok:
                self.onOver(self, x, y)
        elif (event.type == _GDK.BUTTON_RELEASE
              and event.button == 1):
            x, y, ok = self.__check_coords(event.x, event.y)
            if ok:
                self.onClick(self, x, y)
        elif event.type == _GDK.KEY_PRESS:
            key = self.__get_key_string(event.keyval, event.string)
            if key:
                # Get as much milage as possible from the modifiers
                # computation
                try:
                    modifiers = self.__modifier_map[event.state]
                except KeyError:
                    GDK = _GDK
                    modifiers = 0
                    state = event.state
                    if (state & GDK.SHIFT_MASK) == GDK.SHIFT_MASK:
                        modifiers = modShift
                    if (state & GDK.CONTROL_MASK) == GDK.CONTROL_MASK:
                        modifiers = modifiers | modControl
                    self.__modifier_map[state] = modifiers
                self.onKey(self, key, modifiers)

    def __check_coords(self, x, y):
            xoff, yoff, width, height = self.__get_allocation()
            _int = int
            _round = round
            x = _int(_round(x)) - xoff
            y = _int(_round(y)) - yoff
            if x < 0 or y < 0 or x >= width or y >= height:
                return x, y, 0
            return x, y, 1

    __get_key_string = {
        _GDK.Left: keyLeft,
        _GDK.Right: keyRight,
        _GDK.Up: keyUp,
        _GDK.Down: keyDown,
        _GDK.Prior: keyPgUp,
        _GDK.Next: keyPgDn,
        _GDK.Home: keyHome,
        _GDK.End: keyEnd,
        }.get

    # This covers most cases, but may be extended by __event().
    __modifier_map = {
        0: 0,
        _GDK.SHIFT_MASK: modShift,
        _GDK.CONTROL_MASK: modControl,
        _GDK.SHIFT_MASK | _GDK.CONTROL_MASK: modShift | modControl,
        }


class DialogCanvas(InteractiveCanvas):
    def __init__(self, size=(300,300), name="Piddle-GTK"):
        width, height = (int(round(size[0])), int(round(size[1])))
        top = self.__top = gtk.GtkDialog()
        vbox = top.vbox
        frame = self.__frame = gtk.GtkFrame()
        frame.set_shadow_type(gtk.SHADOW_IN)
        da = gtk.GtkDrawingArea()
        button = gtk.GtkButton("Dismiss")
        button.connect("clicked",
                       lambda button, top=top: top.destroy())
        frame.set_border_width(10)
        bbox = self.__bbox = gtk.GtkHButtonBox()
        bbox.set_layout(gtk.BUTTONBOX_END)
        bbox.pack_end(button)
        top.action_area.pack_end(bbox)
        frame.add(da)
        vbox.pack_start(frame)
        InteractiveCanvas.__init__(self, da, top)
        top.set_wmclass("canvas", "Canvas")
        da.realize()
        da.set_usize(width, height)
        top.show_all()
        top.set_icon_name(name)
        top.set_title(name)
        self.ensure_size(width, height)


    # Class-specific interfaces (.get_drawing_area() provided by Canvas):

    def get_toplevel(self):
        return self.__top

    def get_frame(self):
        return self.__frame

    def get_buttonbox(self):
        return self.__bbox


class GTKCanvas(InteractiveCanvas):
    def __init__(self, size=(300,300), name="Piddle-GTK"):
        width, height = (int(round(size[0])), int(round(size[1])))
        #
        top = self.__top = gtk.GtkWindow()
        vbox = self.__vbox = gtk.GtkVBox()
        sbar = self.__sbar = gtk.GtkStatusbar()
        da = gtk.GtkDrawingArea()
        #
        top.add(vbox)
        vbox.pack_start(da)
        vbox.pack_end(sbar, expand=0)
        sbar.set_border_width(2)
        InteractiveCanvas.__init__(self, da, top)
        top.set_wmclass("canvas", "Canvas")
        da.realize()
        da.set_usize(width, height)
        top.show_all()
        top.set_icon_name(name)
        top.set_title(name)
        self.ensure_size(width, height)
        self.__status = None

    def setInfoLine(self, s):
        if self.__status:
            self.__sbar.pop(1)
        if s:
            self.__sbar.push(1, str(s))
        self.__status = s


    # Class-specific interfaces (.get_drawing_area() provided by Canvas):

    def get_toplevel(self):
        return self.__top

    def get_statusbar(self):
        return self.__sbar

    def get_vbox(self):
        return self.__vbox
