#! /usr/bin/env python
#
#  Interactive GTK pylab console
#  Copyright (c) 2006,2007,2008 Nicolas P. Rougier
#
#  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 3 of the License, or (at your option) any later
#  version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  this program. If not, see <http://www.gnu.org/licenses/>.
""" Interactive GTK/Python/Matplotlib console


This console handles python stdin/stderr/stdout redirection and system wide
stdout/stderr redirection (using a pipe), provides history based on the GNU
readline package and basic automatic completion. It also displays matplotlib
figures inline. Each call to the show functions actually produces a
FigureCanvasGTKAgg that is inserted within the console. A 'replot' command has
been added that replot the last figure. Last, but not least, mouse interaction
has been made possible on the last plotted figures.
"""

import os
import sys
import gc
import gtk
import pango
import matplotlib
matplotlib.use('GtkAgg')
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as Canvas

import matplotlib.backends.backend_gtkagg as backend_gtkagg

def draw_if_interactive():
    """
    Is called after every pylab drawing command
    """
    show(console)
backend_gtkagg.draw_if_interactive = draw_if_interactive


import pylab
import matplotlib.pylab
from matplotlib._pylab_helpers import Gcf
import console as cons



# ---------------------------------------------------------------------- replace
def replace (console, canvas, anchor):
    """ Replaces a given canvas with a static image replica """

    figures = console.figures
    view = console.view
    canvas.draw()
    w, h = canvas.get_size_request()
    pixbuf = gtk.gdk.pixbuf_new_from_data (
        canvas.buffer_rgba(0,0), gtk.gdk.COLORSPACE_RGB, True,8,w,h,w*4)
    image = gtk.Image()
    image.set_from_pixbuf (pixbuf)
    for widget in anchor.get_widgets():
        widget.destroy()
    view.add_child_at_anchor(image, anchor)
    image.show()
    gc.collect()

# ----------------------------------------------------------------------- refresh
def refresh (console):
    """ Refreshs all active canvas  """

    figures = console.figures
    for fig in figures:
        figure, canvas, anchor = fig
        canvas.draw()

# ----------------------------------------------------------------------- refresh
def figure_enter(widget,event):
    """ Change cursor to an arrow """

    watch = gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_ARROW)
    widget.window.set_cursor (watch)

# ----------------------------------------------------------------------- refresh
def figure_leave(widget,event):
    """ Change cursor to text cursor """

    cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
    widget.window.set_cursor (cursor)

# ----------------------------------------------------------------------- insert
def insert (console, figure):
    """  Inserts a new canvas for the given figure """

    figures = console.figures
    last_figure = console.last_figure
    figure.set_facecolor ('w')
    view = console.view
    buffer = console.buffer

    # Compute size of the canvas according to current console visible area
    x,y,width,height = console.get_allocation()
    dpi = figure.get_dpi()
    figwidth = figure.get_figwidth() * dpi
    figheight = figure.get_figheight() * dpi
    w = int (width*.75)
    h = int ( (w/figwidth)*figheight)
    if h > height*.75:
        h = int (height*.75)
        w = int ( (h/figheight)*figwidth)
    figure.set_figwidth  (w/dpi)
    figure.set_figheight (h/dpi)
    canvas = Canvas(figure)
    canvas.set_size_request (w,h)
    canvas.show_all()
    console.write ('\n')
    console.write (' ', 'center')
    iter = buffer.get_iter_at_mark(buffer.get_mark('insert'))
    anchor = buffer.create_child_anchor(iter)
    box = gtk.EventBox()
    box.add(canvas)
    box.connect ('enter-notify-event', figure_enter)
    box.connect ('leave-notify-event', figure_leave)
    view.add_child_at_anchor(box, anchor)
    box.show_all()
    for s,func in console.callbacks:
        canvas.mpl_connect(s,func)
    console.write ('\n\n')
    figures.append ( (figure, canvas, anchor) )
    console.last_figure = figure

# ----------------------------------------------------------------------- replot
def replot (console):
    """
    Produces a replot of the last figure and insert it within console. Previous
    figure, if it exists, is transformed into a static image replica and
    inserted in place of the previous figure.
    """

    figures = console.figures
    last_figure = console.last_figure
    if not figures:
        if last_figure:
            insert (console, last_figure)
            return
        else:
            return
    fig = figures[-1]
    figure, canvas, anchor = fig
    replace (console, canvas, anchor)
    figures.remove ( (figure, canvas, anchor) )
    insert (console, figure)
    console.view.scroll_mark_onscreen(console.buffer.get_insert())
    while gtk.events_pending():
        gtk.main_iteration()


# ---------------------------------------------------------------------- connect
def connect (console, s, func):
    """ Append callback to the list of callbacks (to be connected later) """

    console.callbacks.append([s,func])

# ------------------------------------------------------------------------- show
def show (console):
    """ Insert pending figures within console """

    figures = console.figures
    last_figure = console.last_figure
    for manager in Gcf.get_all_fig_managers():
        found = False
        for fig in figures:
            figure, canvas, anchor = fig
            if figure == manager.canvas.figure:
                canvas.draw()
                found = True
                break
        if not found:
            insert (console, manager.canvas.figure)

# ---------------------------------------------------------------- class Console
class Console (cons.Console):
    """ GTK python console """

    def __init__(self, ns_globals={}, ns_locals={}, hfile=None, hsize=100):
        """ Console interface building + initialization"""

        cons.Console.__init__(self, ns_globals, ns_locals, hfile, hsize)
        self.buffer.create_tag('center',
                               justification=gtk.JUSTIFY_CENTER,
                               font='Mono 10')
        self.figures = []
        self.callbacks = []
        self.last_figure = None
        self.view.connect ('button-press-event', self.button_press_event)

    def banner(self):
        """ Display a fake python banner """

        python_version = sys.version.split(' ')[0]
        pylab_version = matplotlib.__version__
        self.write ('GTK Pylab console\n', 'banner_title')
        self.write ('(using python %s and matplotlib %s)\n\n'
                    % (python_version, pylab_version), 'banner_subtitle')


    def button_press_event (self, *args):
        """ Refresh drawing """

        for fig in self.figures:
            figure, canvas, anchor = fig
            canvas.draw()
        return False


if __name__ == "__main__":
    try:
        from functools import partial
    except ImportError:
        def partial(func, *args, **keywords):
            def newfunc(*fargs, **fkeywords):
                newkeywords = keywords.copy()
                newkeywords.update(fkeywords)
                return func(*(args + fargs), **newkeywords)
            newfunc.func = func
            newfunc.args = args
            newfunc.keywords = keywords
            return newfunc


    window = gtk.Window(gtk.WINDOW_TOPLEVEL)
    hfile = os.path.expanduser("~/.pyhistory")
    console = Console({}, {}, hfile, 100)

    pylab.show = partial (show, console)
    matplotlib.pylab.show = pylab.show
    matplotlib.pyplot.show = pylab.show
    pylab.connect = partial (connect, console)
    matplotlib.pylab.connect = pylab.connect
    console.globals['replot'] = partial (replot, console)

    window.set_position(gtk.WIN_POS_CENTER)
    window.set_default_size(640,480)
    window.set_border_width(0)
    window.connect('destroy-event', gtk.main_quit)
    window.connect('delete-event', gtk.main_quit)
    window.add (console)
    window.show_all()
    console.grab_focus()
    console.execute ("from pylab import *")
    console.banner()
    if len(sys.argv) > 1:
        console.execute ("execfile('%s')" % sys.argv[1])
    console.prompt()
    gtk.main()
