"""
  Copyright 2005 Allen B. Downey

    This file contains wrapper classes I use with tkinter.  It is
    mostly for my own use; I don't support it, and it is not very
    well documented.

    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, see http://www.gnu.org/licenses/gpl.html
    or write to the Free Software Foundation, Inc., 51 Franklin St, 
    Fifth Floor, Boston, MA  02110-1301  USA
"""

from math import *
from Tkinter import *

import threading, time, os, signal, sys

class Thread(threading.Thread):
    """this is a wrapper for threading.Thread that improves
    the syntax for creating and starting threads.  See Appendix A
    of The Little Book of Semaphores
    """
    def __init__(self, target, *args):
        threading.Thread.__init__(self, target=target, args=args)
        self.start()

class Semaphore(threading._Semaphore):
    """this is a wrapper class that makes signal and wait
    synonyms for acquire and release, and also makes signal
    take an optional argument.
    """
    wait = threading._Semaphore.acquire
    
    def signal(self, n=1):
        for i in range(n): self.release()

    def value(self):
        return self._Semaphore__value



# Gui provides wrappers for many of the methods in the Tk
# class; also, it keeps track of the current frame so that
# you can create new widgets without naming the parent frame
# explicitly.

class Gui(Tk):
    def __init__(self, debug=False):
        Tk.__init__(self)
        self.debug = debug
        # turning on debugging changes the behavior of Gui.fr so
        # that the nested frame structure is apparent
        self.frame = self          # the current frame
        self.frames = []           # the stack of nested frames

    # end frame
    def endfr(self):
        self.frame = self.frames.pop()

    popfr = endfr

    # push frame
    def pushfr(self, frame):
        self.frames.append(self.frame)
        self.frame = frame

    # top level window
    def tl(self, *args, **options):
        self.pushfr(Toplevel(*args, **options))
        return self.frame

    """The following widget wrappers all invoke widget to
    create and pack the new widget.  The first four positional
    arguments determine how the widget is packed.  Some widgets
    take additional positional arguments.  In most cases, the
    keyword arguments are passed as options to the widget
    constructor.

    The default pack arguments are
    side=TOP, fill=NONE, expand=0, anchor=CENTER
    Widgets that use these defaults can just pass along
    args and options unmolested.  Widgets (like fr and en)
    that override them take positional arguments with
    appropriate defaults.
    """

    # frame
    def fr(self, side=TOP, fill=BOTH, expand=1, anchor=CENTER, **options):
        # save the current frame, then create the new one
        self.frames.append(self.frame)
        if self.debug:
            options['bd'] = 5
            options['relief'] = RIDGE
        self.frame = self.widget(Frame, side, fill, expand, anchor, **options)
        return self.frame

    # entry
    def en(self, side=TOP, fill=BOTH, expand=1, anchor=CENTER,
           text='', **options):
        en = self.widget(Entry, side, fill, expand, anchor, **options)
        en.insert(0, text)
        return en

    # canvas
    def ca(self, *args, **options):
        return self.widget(GuiCanvas, *args, **options)

    # label
    def la(self, *args, **options):
        return self.widget(Label, *args, **options)

    # button
    def bu(self, *args, **options):
        return self.widget(Button, *args, **options)

    # menu button
    def mb(self, *args, **options):
        mb = self.widget(Menubutton, *args, **options)
        mb.menu = Menu(mb, relief=SUNKEN)
        mb['menu'] = mb.menu
        return mb

    # menu item
    def mi(self, mb, label='', **options):
        mb.menu.add_command(label=label, **options)        

    # text entry
    def te(self, *args, **options):
        return self.widget(Text, *args, **options)

    # scrollbar
    def sb(self, *args, **options):
        return self.widget(Scrollbar, *args, **options)

    class ScrollableText:
        def __init__(self, gui,
                     *args, **options):
            self.frame = gui.fr(*args, **options)
            self.scrollbar = gui.sb(RIGHT, fill=Y)
            self.text = gui.te(LEFT, wrap=WORD,
                                 yscrollcommand=self.scrollbar.set)
            self.scrollbar.config(command=self.text.yview)
            gui.endfr()
            
    # scrollable text
    # returns a ScrollableText object that contains a frame, a
    # text entry and a scrollbar.
    # note: the options provided to st apply to the frame only;
    # if you want to configure the other widgets, you have to do
    # it after invoking st
    def st(self, *args, **options):
        return self.ScrollableText(self, *args, **options)

    # this is the mother of all widget constructors.  the constructor
    # argument is the function that will be called to build
    # the new widget
    def widget(self, constructor, side=TOP, fill=NONE, expand=0, anchor=CENTER,
               *args, **options):
        widget = constructor(self.frame, *args, **options)
        widget.pack(side=side, fill=fill, expand=expand, anchor=anchor)
        return widget

class GuiCanvas(Canvas):
    """this is a wrapper for the Canvas provided by tkinter.
    The primary difference is that it supports coordinate
    transformations, the most common of which is the CanvasTranform,
    which make canvas coordinates Cartesian (origin in the middle,
    positive y axis going up).

    It also provides methods like create_circle that provide a
    nice interface to the underlying canvas methods.
    """
    def __init__(self, w, transforms=None, **options):
        Canvas.__init__(self, w, **options)
        self.transforms = transforms or []

    def add_transform(self, transform):
        self.transforms.append(transform)
    
    def trans(self, coords):
        for trans in self.transforms:
            coords = trans.trans_list(coords)
        return coords

    def invert(self, coords):
        for trans in self.transforms:
            coords = trans.invert_list(coords)
        return coords

    def create_circle(self, x, y, r, fill='', **options):
        options['fill'] = fill
        coords = self.trans([[x-r, y-r], [x+r, y+r]])
        tag = Canvas.create_oval(self, coords, options)
        return tag
    
    def create_oval(self, coords, fill='', **options):
        options['fill'] = fill
        return Canvas.create_oval(self, self.trans(coords), options)

    def create_rectangle(self, coords, fill='', **options):
        options['fill'] = fill
        return Canvas.create_rectangle(self, self.trans(coords), options)

    def create_line(self, coords, fill='black', **options):
        options['fill'] = fill
        tag = Canvas.create_line(self, self.trans(coords), options)
        return tag
    
    def create_polygon(self, coords, fill='', **options):
        options['fill'] = fill
        return Canvas.create_polygon(self, self.trans(coords), options)

    def create_text(self, coord, text='', fill='black', **options):
        options['text'] = text
        options['fill'] = fill
        return Canvas.create_text(self, self.trans([coord]), options)

    def create_image(self, coord, image, **options):
        options['image'] = image
        return Canvas.create_image(self, self.trans([coord]), options)

    def postscript(self, filename='gui.eps'):
        ps = Canvas.postscript(self)
        fp = open(filename, 'w')
        fp.write(ps)
        fp.close()


class Transform:
    def trans_list(self, points):
        return [self.trans(p) for p in points]

    def invert_list(self, points):
        return [self.invert(p) for p in points]
    

class CanvasTransform(Transform):
    def __init__(self, width, height, xscale=1, yscale=1):
        self.width = width
        self.height = height
        self.xscale = xscale
        self.yscale = yscale
    
    def trans(self, p):
        x =  p[0] * self.xscale + self.width/2
        y = -p[1] * self.yscale + self.height/2      
        return [x, y]


class RotateTransform(Transform):

    def __init__(self, theta):
        self.theta = theta

    def rotate(self, p, theta):
        s = sin(theta)
        c = cos(theta)
        x =   c * p[0] + s * p[1]
        y =  -s * p[0] + c * p[1]
        return [x, y]
    
    def trans(self, p):
        return self.rotate(p, self.theta)

    def invert(self, p):
        return self.rotate(p, -self.theta)


class SwirlTransform(RotateTransform):

    def trans(self, p):
        d = sqrt(p[0]*p[0] + p[1]*p[1])
        return self.rotate(p, self.theta*d)

    def invert(self, p):
        d = sqrt(p[0]*p[0] + p[1]*p[1])
        return self.rotate(p, -self.theta*d)


class Callable:
    """this class is used to wrap a function and its arguments
    into an object that can be passed as a callback parameter
    and invoked later.  It is from the Python Cookbook 9.1, page 302
    """
    
    def __init__(self, func, *args, **kwds):
        self.func = func
        self.args = args
        self.kwds = kwds

    def __call__(self):
        return apply(self.func, self.args, self.kwds)

    def __str__(self):
        return self.func.__name__


class Watcher:
    """this class solves two problems with multithreaded
    programs in Python, (1) a signal might be delivered
    to any thread (which is just a malfeature) and (2) if
    the thread that gets the signal is waiting, the signal
    is ignored (which is a bug).

    The watcher is a concurrent process (not thread) that
    waits for a signal and the process that contains the
    threads.  See Appendix A of The Little Book of Semaphores.

    I have only tested this on Linux.  I would expect it to
    work on the Macintosh and not work on Windows.
    """
    
    def __init__(self):
        """ Creates a child thread, which returns.  The parent
            thread waits for a KeyboardInterrupt and then kills
            the child thread.
        """
        self.child = os.fork()
        if self.child == 0:
            return
        else:
            self.watch()

    def watch(self):
        try:
            os.wait()
        except KeyboardInterrupt:
            # I put the capital B in KeyBoardInterrupt so I can
            # tell when the Watcher gets the SIGINT
            print 'KeyBoardInterrupt'
            self.kill()
        sys.exit()

    def kill(self):
        try:
            os.kill(self.child, signal.SIGKILL)
        except OSError: pass


